我家门前有两棵树,一棵是二叉树,另一棵也是二叉树。
遍历一棵二叉树常用的有四种方法,前序(PreOrder)、中序(InOrder)、后序(PastOrder)还有层序(LevelOrder)。
前中后序三种遍历方式都是以根节点相对于它的左右孩子的访问顺序定义的。例如根->左->右便是前序遍历,左->根->右便是中序遍历,左->右->根便是后序遍历。
而层序遍历是一层一层来遍历的。
树的前中后序遍历是个递归的定义,在遍历到根节点的左/右子树时,也要遵循前/中/后序遍历的顺序,例如下面这棵树:
前序遍历:ABDECFG
中序遍历:DBEAFCG
后序遍历:DEBFGCA
层序遍历:ABCDEFG
树的结点结构体声明如下:
语言:C语言(为了省事用到了C++的栈,因为C语言要用栈的话要自己重新写一个出来,就偷了个懒)
编译器:VS
typedef char DataType;
typedef struct TreeNode{
DataType data;
struct TreeNode *left;
struct TreeNode *right;
}TreeNode;
对于一棵树的前序遍历,递归的写法是最简单的(写起来),就是将一个大的问题转化为几个小的子问题,直到子问题可以很容易求解,最后将子问题的解组合起来就是大问题的解。
先放代码,如果看完觉得不太清楚可以看看下面的详细步骤图解。
void PreOrder(const TreeNode *root)
{
if (root == NULL) //若结点为空
{
printf("# ");
return;
}
printf("%c ", root->data); //输出根节点的值
PreOrder(root->left); //前序访问左子树
PreOrder(root->right); //前序访问右子树
}
比如说还是上面的这颗树:
还是先上代码:
void PreOrderLoop(TreeNode *root)
{
std::stack s;
TreeNode *cur, *top;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
printf("%c ", cur->data);
s.push(cur);
cur = cur->left;
}
top = s.top();
s.pop();
cur = top->right;
}
}
非递归的写法比递归写法要麻烦一点,要用到栈来存储树的结点,在理解非递归方法的时候要重点理解栈中保存的元素的共同点是什么,在前序访问中,栈中元素都是自己和自己的左孩子都访问过了,而右孩子还没有访问到的节点,如果不太懂可以看下面的详细步骤图解。
对于一棵树的中序遍历,和前序一样,可以分为递归遍历和非递归遍历,递归遍历是相对简单的,还是子问题思想,将一个大问题分解,直到可以解决,最后解决整个大问题。
还是先上代码:
void InOrder(const TreeNode *root)
{
if (root == NULL) //判断节点是否为空
{
printf("# ");
return;
}
InOrder(root->left); //中序遍历左子树
printf("%c ", root->data); //访问节点值
InOrder(root->right); //中序遍历右子树
}
发现不为空,继续访问左子树
发现不为空,继续访问左子树
这时root为空了,就返回去访问它的根节点,刚才的访问只是路过,并没有真正地遍历节点的信息,在返回途中才是真正地遍历到了节点的信息。
访问到了D节点,下来要访问的是D的右孩子,因为D的左孩子已经访问过了。
发现还是空,就返回,而它的根节点D也访问过了,那么就继续返回,该访问D节点的父节点B了。
B访问过后下来要访问的是B的右孩子,因为是从B的左子树回来的路,B的左孩子已经访问过了。
然后和访问D一样,->左孩子,为空,返回访问根节点E,->右孩子,为空(这部分就不画了,和D节点的访问是一样的),最后返回,B已经访问过了,就继续返回,至此,整颗树的左子树访问完了。
3. 访问B的根节点A
4. 遍历A的右子树
遍历右子树的过程和左子树一样,还是左->根->右的中序遍历下去,直到遍历到G的右孩子,发现为空,就返回,因为右子树都遍历过了,所以可以一直返回到root为A节点的那一层递归,整个遍历结束。
最后输出的中序访问序列为:DBEAFCG
中序访问的非递归写法和前序一样,都要用到一个栈来辅助存储,不一样的地方在于前序访问时,栈中保存的元素是右子树还没有被访问到的节点的地址,而中序访问时栈中保存的元素是节点自身和它的右子树都没有被访问到的节点地址。
先上代码:
void InOrderLoop(TreeNode *root)
{
std::stack s;
TreeNode *cur;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
s.push(cur);
cur = cur->left;
}
cur = s.top();
s.pop();
printf("%c ", cur->data);
cur = cur->right;
}
}
最后输出的中序访问序列为:DBEAFCG
后序遍历还是分递归版本和非递归版本,后序遍历的递归版本和前序中序很相似,就是输出根节点值的时机不同,而后序遍历的非递归版本则要比前序和中序的要难一些,因为在返回根节点时要分从左子树返回和右子树返回两种情况,从左子树返回时不输出,从右子树返回时才需要输出根节点的值。
先上代码:
void PostOrder(TreeNode *root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
后序遍历的递归版本和前中序非常相似,就是输出根节点值的时机不同,详细图解这里就不画了,可以联系前中序的递归版本来理解。
后序遍历的非递归同样要借助一个栈来保存元素,栈中保存的元素是它的右子树和自身都没有被遍历到的节点,与中序遍历不同的是先访问右子树,在回来的时候再输出根节点的值。需要多一个last指针指向上一次访问到的节点,用来确认是从根节点的左子树返回的还是从右子树返回的。
先上代码:
void PostOrderLoop(TreeNode *root)
{
std::stack s;
TreeNode *cur, *top, *last = NULL;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
s.push(cur);
cur = cur->left;
}
top = s.top();
if (top->right == NULL || top->right == last){
s.pop();
printf("%c ", top->data);
last = top;
}
else {
cur = top->right;
}
}
}
最后输出的中序访问序列为:DEBFGCA
层序遍历是比较接近人的思维方式的一种遍历方法,将二叉树的每一层分别遍历,直到最后的叶子节点被全部遍历完,这里要用到的辅助数据结构是队列,队列具有先进先出的性质。
上代码:
void LevelOrder(TreeNode *root)
{
std::queue q;
TreeNode *front;
if (root == NULL)return;
q.push(root);
while (!q.empty())
{
front = q.front();
q.pop();
if (front->left)
q.push(front->left);
if (front->right)
q.push(front->right);
printf("%c ", front->data);
}
}
层序遍历的思路是,创建一个队列,先将根节点(A)入队,然后用front指针将根节点记下来,再将根节点出队,接下来看front节点(也就是刚才的根节点)有没有左孩子或右孩子,如果有,先左(B)后右(C)入队,最后输出front节点的值,只要队列还不为空,就说明还没有遍历完,就进行下一次循环,这时的队头元素(front)则为刚才入队的左孩子(B),然后front出队,再把它的左右孩子拉进来(如果有),因为队列的先进先出性质,B的左右孩子DE是排在C后面的,然后输出B,下一次循环将会拉人C的左右孩子FG,最后因为FG没有左右孩子,一直出队,没有入队元素,队列迟早会变为空,当队列为空时,整颗树就层序遍历完成了,结束循环。
最后输出的层序访问序列为:ABCDEF
编辑于2018-8-27 16:55:37