一、树的遍历
树的遍历有两大类四种方式,分别是深度优先遍历和广度优先遍历。
(一)深度优先遍历
- 先序遍历
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树; - 中序遍历(二叉树独有)
(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历右子树; - 后序遍历
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点;
(二)广度优先遍历
- 层次遍历:一层一层的访问,从上到下,从左到右的顺序。
(三)C语言实现
使用递归实现这些遍历比较简单,重点理解递归的用法即可,而使用非递归方式则比较麻烦,这里递归与非递归方式都会给出代码,为了方便,被遍历的树为一颗二叉搜索树,而对树结点的操作为打印输出操作。
(1)递归实现
//头文件:Tree.h
#ifndef TREE_H_INCLUDED
#define TREE_H_INCLUDED
struct BinarySearchTree{
int element;
struct BinarySearchTree* left;
struct BinarySearchTree* right;
};
typedef struct BinarySearchTree* Tree;
Tree insertEle(Tree, int);
void preOrderByRec(Tree);
void inOrderByRec(Tree);
void postOrderByRec(Tree);
void levelOrder(Tree);
#endif // TREE_H_INCLUDED
//遍历实现文件:Tree.c
#include <stdio.h>
#include <stdlib.h>
#include "Tree.h"
//插入新结点
Tree insertEle(Tree tree, int data){
if(tree){
if(tree->element > data){
tree->left = insertEle(tree->left, data);
}else if(tree->element < data){
tree->right = insertEle(tree->right, data);
}else{
printf("树中已经有该元素,不再插入\n");
}
}else{
tree = (Tree)malloc(sizeof(struct BinarySearchTree));
tree->element = data;
tree->left = tree->right = NULL;
}
return tree;//注意这里一定要返回该指针!!
}
/*
递归实现三种深度优先遍历时,代码基本一致
不同的是对元素操作的时机不同
*/
void preOrderByRec(Tree tree){
if(tree){
printf("%d\n", tree->element);//最先访问
preOrderByRec(tree->left);//遍历左子树
preOrderByRec(tree->right);//遍历右子树
}
}
void inOrderByRec(Tree tree){
if(tree){
inOrderByRec(tree->left);//先遍历左子树
printf("%d\n", tree->element);//访问结点
inOrderByRec(tree->right);//最后遍历右子树
}
}
void postOrderByRec(Tree tree){
if(tree){
postOrderByRec(tree->left);//先遍历左子树
postOrderByRec(tree->right);//再遍历右子树
printf("%d\n", tree->element);//最后访问结点
}
}
/*层次遍历需要使用一个队列,这里只给出伪代码*/
/*
void levelOrder(Tree tree){
Queue Q;
Tree T;
if(!tree) return;//空树则直接返回
Q = createQueue();//创建空队列Q
addQ(Q, tree);
//利用了队列先进先出的性质
//往队列里添加第k层时,第k层由第k-1层的结点生出来
//依次添加k-1层每个结点的左右子结点
//比如,一颗树按层划分为(12)-(5,14)-(4,8,17)-(23)
//那么往队列添加删除的顺序为(加号为添加,减号为删除):
//+12, -12, +5, +14, -5, +4, +8, -14, +17, -4, -8, -17, +23, -23
while(!isEmpty(Q)){
T = deleteQ(Q);
printf("%d\n", T->element);
if(T->left){
addQ(Q, T->left);
}
if(T->right){
addQ(Q, T->right);
}
}
}
*/
//main入口:main.c
#include <stdio.h>
#include <stdlib.h>
#include "Tree.h"
int main()
{
Tree tree = NULL;
tree = insertEle(tree, 12);
tree = insertEle(tree, 5);
tree = insertEle(tree, 14);
tree = insertEle(tree, 4);
tree = insertEle(tree, 8);
tree = insertEle(tree, 23);
tree = insertEle(tree, 17);
printf("先序遍历结果:\n");
preOrderByRec(tree);
printf("\n");
printf("中序遍历结果:\n");
inOrderByRec(tree);
printf("\n");
printf("后序遍历结果:\n");
postOrderByRec(tree);
printf("\n");
return 0;
}
//先序遍历结果:
//12
//5
//4
//8
//14
//23
//17
//中序遍历结果:
//4
//5
//8
//12
//14
//17
//23
//后序遍历结果:
//4
//8
//5
//17
//23
//14
//12
(2)非递归实现
因为递归本质上使用了一个栈,所以将递归化为非递归,一般额外需要使用一个栈,用该栈来模拟递归,此时循环的条件除了递归里的条件,还有一个就是栈不为空,二叉树的先序遍历和中序遍历化为非递归的主要代码差不多,所以先说这两个,这里也只给出伪代码。
void traverse(Tree tree){
Stack stack = createStack(Maxsize);
Tree T = tree;//注意,这里需要用一个局部指针来代替传进来的tree,以免把原树的结构改变了
//循环的条件二选一:一是结点不为空,二是栈不为空,任何一个满足都可以继续循环
while(T || !isEmpty(stack)){
if(T){
//printf("%d\n", T->element);//先序遍历
push(stack, T);
T = T->left;
}else{
pop(stack, T);
printf("%d\n", T->element);//中序遍历
T = T->right;
}
}
}
后序遍历的非递归实现稍微麻烦一点,这里需要使用一个栈、一个记录当前访问的结点,一个记录上次访问的结点,后序遍历里,对于任意一个结点,只有其右子结点被访问过了,该结点才会被访问,所以我们可以利用这个条件,先出栈一个结点,如果该节点的右子结点没有被访问,那么这个结点再进栈,然后进入右子结点遍历。
后序遍历说明图
如上图,先依次将个左子结点入栈,此时栈Stack = [1,2],然后开始出栈,结点2没有右结点,因此打印输出,然后继续出栈,结点1有右子结点并且此时右子结点并没有访问,所以结点1再次进栈,并将结点3入栈,此时栈Stack = [1,3],此时没有结点可以入栈了,于是结点3出栈,结点3没有右子结点,因此打印输出,然后结点1出栈,因为此时上一次访问的结点是右子节点,因此此时结点1打印,此时栈空,停止程序。伪代码如下。
//后序遍历非递归实现伪代码
void postTraverse(Tree tree){
Tree curTree = tree;//当前操作的结点
Tree lastTree = NULL;//上次打印输出的结点
Stack stack = createStack(MaxSize);//建栈
//向左遍历进入最左边的元素
while(curTree){
push(stack, curTree);
curTree = curTree->left;
}
//考虑上图的思路过程,判断条件为栈非空
while(!isEmpty(stack)){
pop(stack, curTree);
//访问根结点的条件是该结点没有右子结点或者右子结点已经访问过
if(curTree->right == NULL || curTree->right == lastTree){
printf("%d\n", curTree->element);
lastTree = curTree;
}else{
push(stack, curTree);//根结点再次入栈
//右子树入栈
curTree = curTree->right;
while(curTree){
push(stack, curTree);
curTree = curTree->left;
}
}
}
}
(3)总结
二叉树的非递归遍历程序里关于循环条件有点绕,因为每个结点既可以看作是根结点、也可以看作子结点,这种灵活性增加了思考的难度,但总的来说,不管哪种遍历方式,都需要在整棵树的前提下,先向左找到最左的结点,这里形成了一条路径,然后沿着该路径回溯,判断出栈操作或向右遍历,而向右遍历的时候看起来只操作了一次(T = T->right)就又开始向左迭代找最左的元素,但这也符合逻辑,即先左子树后右子树的规定,因为把右子树单独看作一棵树,这颗树也有自己的左子树,如果想不清楚的话,按照程序多画画树,就好理解了。