二叉查找树的C语言实现(二)
接着上次的话题。这次我们要讨论,二叉查找树的中序遍历和后序遍历(递归和非递归),另外还有先序遍历(非递归)
1.中序遍历(递归)
static void __in_order(struct bnode_info *bnode, void (*todo)(struct bnode_info *bnode)) { if (bnode != NULL) { __in_order(bnode->lchild, todo); todo(bnode); __in_order(bnode->rchild, todo); } } static void btree_in_order(struct btree_info *info, void (*todo)(struct bnode_info *bnode)) { assert(todo != NULL && info != NULL); __in_order(info->root, todo); }其实很简单了,就是把语句的顺序换一换就可以了。
--in_order--
5 16 24 26 50 80
这是运行的结果。有没有发现,数字是按从小到大排序的!如果一棵二叉查找树的节点值是数值,那么中序遍历的结果为升序排列的数组。
2.中序遍历-非递归
static void btree_in_order_norecur(struct btree_info *info, void (*todo)(struct bnode_info *bnode)) { assert(info != NULL && todo != NULL); if (btree_is_empty(info)) return ; //栈空间的创建 struct stack_info *stack = (struct stack_info *)malloc(sizeof(struct stack_info)); assert(stack != NULL); //栈初始化 stack_init(stack); struct bnode_info *cur = info->root; while (!stack->is_empty(stack) || cur != NULL) { if (cur != NULL) { stack->push(stack, &cur, sizeof(cur)); cur = cur->lchild; } else { stack->pop(stack, &cur, sizeof(cur)); todo(cur); cur = cur->rchild; } } stack_destroy(stack); free(stack); }思路是创建一个栈,顺着树根,一直往左边的节点走,一路压栈,走到最左边的那个节点,也压栈。这时候当前节点为NULL,开始出栈,弹出的这个元素就是子树的树根,todo(cur), 然后遍历右子树。对于右子树,也是同样的方法,一路向左,依次压栈......
3.先序遍历-非递归
static void btree_pre_order_norecur(struct btree_info *info, void (*todo)(struct bnode_info *bnode)) { assert(info != NULL && todo != NULL); if (btree_is_empty(info)) { return ; } //栈空间的创建 struct stack_info *stack = (struct stack_info *)malloc(sizeof(struct stack_info)); assert(stack != NULL); //栈初始化 stack_init(stack); struct bnode_info *cur = info->root; while (!stack->is_empty(stack) || cur != NULL) { if (cur != NULL) { todo(cur); stack->push(stack, &cur, sizeof(cur)); cur = cur->lchild; }else{ stack->pop(stack, &cur, sizeof(cur)); cur = cur->rchild; } } stack_destroy(stack); free(stack); }其实和上面的代码类似,也是用了栈。首先cur指向树根,既然是先序,直接todo(cur); 之后要把当前节点压栈,因为后面还要利用这个节点找到它的右子树,再然后 cur = cur->lchild, 也就是遍历左子树,一直向左,直到左子树为空,这时候就遍历右子树,于是出栈一个节点,得到他的右孩子,进入下一轮循环。
4.后序遍历--非递归
后序遍历的非递归思路应该是所有顺序中最难的一个了。
错误的思路介绍:
1.首先一路向左,把从树根开始,到树根的左孩子,再到左孩子的左孩子,全部压栈;
2.接着,取栈顶的元素,看看他有没有右孩子,转到3或者4;
3.如果没有,就弹出这个元素,并且打印(也可以是别的操作)这个元素,然后回到2;
4.如果有,就把右孩子作为新的树根,回到1;
以上的思路是错误的,为什么呢?我们结合代码来说明,下面请看错误的代码。
static void btree_post_order_norecur_wrong(struct btree_info *info, void (*todo)(struct bnode_info *bnode)) { assert(info != NULL && todo != NULL); if (btree_is_empty(info)) { return ; } //栈空间的创建 struct stack_info *stack = (struct stack_info *)malloc(sizeof(struct stack_info)); assert(stack != NULL); //栈初始化 stack_init(stack); struct bnode_info *cur = info->root; struct bnode_info *pre = NULL; while (!stack->is_empty(stack) || cur != NULL) { if (cur != NULL) { stack->push(stack, &cur, sizeof(cur)); todo(cur); printf("in stack\n"); cur = cur->lchild; } else { stack->top(stack, &cur, sizeof(cur)); todo(cur); printf("get top stack\n"); if (cur->rchild != NULL) { todo(cur); printf("his rchild not null\n"); cur = cur->rchild; } else { stack->pop(stack, &cur, sizeof(cur)); todo(cur);//这个是真正的打印,其他地方只是为了打印日志。 printf(" -----ok \n"); cur = NULL;//for out stack continue } } } stack_destroy(stack); free(stack); }
为了说明清楚,我加了很多打印的地方。运行结果如下(括号里面的是我的解释)
----wrong:post_order_norecur---
50 in stack
24 in stack
16 in stack
5 in stack
5 get top stack
5 ok (这个是对的)
16 get top stack
16 ok (这个是对的)
24 get top stack (24第一次 get top)
24 his rchild not null
26 in stack
26 get top stack
26 ok (这个是对的)
24 get top stack (24第二次 get top, 其实他的右子树已经遍历了,可是后面发现又开始遍历右子树)
24 his rchild not null
26 in stack
26 get top stack
26 ok
24 get top stack(24第三次 get top, 后面再次遍历右子树)
24 his rchild not null
……
……
后面的日志太长了,因为陷入了死循环。程序就是反复地 取栈顶元素24,遍历他的右子树
分析到这里,我们看出了症结,第一次取栈顶元素24没有错,是为了遍历他的右子树。第二次取的时候,右子树已经遍历过了,这时候就应该让24出栈,并且打印。
可是怎么知道栈顶元素的右子树是否已经遍历过了?方法有很多,这里我们采用这样一种方法:根据后序遍历的特性,如果24是第一次取(就是get top 的操作)且右孩子不为空,那么最近遍历的元素,一定不是24的右孩子(因为还没有遍历);如果24是第二次取,也就是说他的右子树遍历过了,那么最近遍历的那个元素,一定是24的右孩子。
所以,我们把上面的错误思路改一下:
1.从树根开始,到树根的左孩子,再到左孩子的左孩子,全部压栈;
2.接着,取栈顶的元素,看看他有没有右孩子,没有就转到3,有就转到4;
3.弹出这个元素,并且打印(也可以是别的操作)这个元素,还要记录这个元素作为最近打印的元素,然后回到2;
4.再看看他的右孩子是否等于最近打印的元素,等于转到5 , 不等于转到6;
5.说明他的右子树已经遍历了,转到3;
6. 把右孩子作为新的树根,回到1;
看看正确的代码吧:
static void btree_post_order_norecur(struct btree_info *info, void (*todo)(struct bnode_info *bnode)) { assert(info != NULL && todo != NULL); if (btree_is_empty(info)) return ; //栈空间的创建 struct stack_info *stack = (struct stack_info *)malloc(sizeof(struct stack_info)); assert(stack != NULL); //栈初始化 stack_init(stack); struct bnode_info *cur = info->root; struct bnode_info *pre = NULL; // 为了记录最近打印的节点 while (!stack->is_empty(stack) || cur != NULL) { if (cur != NULL) { stack->push(stack, &cur, sizeof(cur)); cur = cur->lchild; //一路向左压栈 } else { stack->top(stack, &cur, sizeof(cur)); if ((cur->rchild != NULL) && (cur->rchild != pre)) //存在右子树,且右子树没有遍历 { cur = cur->rchild; //遍历右子树 } else // 没有右孩子或者右子树已经遍历的情况 { stack->pop(stack, &cur, sizeof(cur)); todo(cur); pre = cur;//更新最近打印的节点 cur = NULL; } } } stack_destroy(stack); free(stack); }
运行后得到正确的结果:
5 16 26 24 80 50
===============================================================================================================================
5.后序遍历--递归
这个很简单,其实递归遍历思路都一样,先序中序后序的区别就是在于 todo(bnode) 语句的位置不一样。直接上代码:
static void __post_order(struct bnode_info *bnode,void (*todo)(struct bnode_info *bnode)) { if (bnode != NULL) { __post_order(bnode->lchild, todo); __post_order(bnode->rchild, todo); todo(bnode); } } static void btree_post_order(struct btree_info *info, void (*todo)(struct bnode_info *bnode)) { __post_order(info->root, todo); }
今天就说到这里,还有一些内容,下次再见!