数据结构——树(5)——树的先序,中序,后序,层次遍历
树的存储结构
之前我们提到过堆是一棵特殊的树,在堆的存储方式中,我们选择了数组的方式去存储。因为只要知道一个节点的位置我们就能找到其他的节点的位置。但是前提是堆是一棵完全二叉树。树的结构多种多样,没人规定说一个节点只能有一个孩子。
我们先看下图,一个最简单的二叉树:
怎么表达?看到箭头,我们应该立马反应过来一个基本的工具,没错就是指针,应该立马反应过来一个数据结构,那就是链表!
对吧,把value换成我们的A B C,分别定义两个指针,一个指向左边的节点,一个指向右边的节点,其他的节点类似,就能把一棵二叉树表达出来了。很完美对吧。那么我们怎么用c++来描述这一个数据结构呢?代码如下:
struct Tree{
string value;
Tree *left;
Tree *right;
};
代码对应的结构图应该是这样的:
但肯定有人问,那么三叉树呢?n叉树呢是不是也可以同样的方式来定义呢?当然!就先说说三叉树,其实三叉树就是在中间多了一个往下的箭头对吧,那么我们的数据结构按上面的结构,多用一个指向中间的指针不就完事了?看下图:
那么 n 叉树呢?这个时候我们就发现如果按照上述的定义,太麻烦了,代码都得多几十行,而且不好记忆。那么我们可以把这些指针存进一个vector中,就像这样:
struct Tree {
string value;
Vector<Tree *> children;
};
这个vector里面存储着指向他们孩子节点的指针。就像这样:
那么树的存储结构在内存中是怎么样的呢?看下图
树的四种遍历方式
我们先看一颗树:
这是一棵简单的二叉树,现在我们来看看什么叫树的遍历:
遍历树的节点并在每个节点执行一些操作的过程称为遍历或行走树(traversing or walking the tree)。在许多情况下,将需要按照数值的的顺序遍历树 这种处理在左右子树的递归调用之间的节点的方法被称为中序遍历(inorder traversal)。 另外还经常发生另外两种类型的树遍历,它们被称为前序遍历和后序遍历(preorder and postorder traversals)。
先序遍历(preOrder)
遍历树的节点并在每个节点执行一些操作的过程称为遍历树,这是我们讲遍历之前提到过的,那么我们就以这个模式来讲解这个部分的内容。操作我们就用最简单的输出数据来实现。这样先序遍历的步骤就是:
- Do something
- Go left
- Go right
结合代码跟上面的树我们来讲解一下具体的步骤
void preOrder(Tree *tree){
if (tree == NULL) return; //如果树为空,直接返回函数
cout << tree -> value << endl;
preOrder(tree -> left);
preOrder(tree -> right);
}
首先,判断树是否为空,是,则直接返回,否则进行下一步。先执行操作(这里是输出节点的值),为Grumpy,然后继续先序遍历节点的左子树,此时的tree指向的是doc节点,判断这子树是否为空?结果是否,因为doc下面还有子节点(孩子分别为Bashful和Dopey),判断为否后执行操作,输出Doc,继续先序遍历,此时tree指向Bashful,判断这子树是否为空,结果为否!(这是个叶子,没有子节点,但是也是一棵孤立的树),执行输出操作,输出Bashful。继续执行先序遍历操作,此时指针指向Bashful的左孩子,由于它没有孩子,于是判断这子树为空,返回程序(return;)。此时指针往上回退,对右边进行先序遍历,输出Dopey。判断这个节点有无孩子,继续执行上述操作。这样子,我们可以看到先序遍历是先遍历左边的子树,再遍历右边的子树。输出为:
中序遍历(inOrder)
中序遍历的步骤为:
- Go left
- Do something
- Go right
看一段代码:
void inOrder(Tree *tree){
if (tree == NULL) return;
inOrder(tree -> left);
cout << tree -> value << endl;
inOrder(tree -> right);
}
首先判断树是否为空,是直接返回,否则继续。继续中序遍历树的左节点,在这个例子中,一开始的指针指向的是Grumpy,因为树不为空,因此遍历它的左节点,也就是这个时候指针指向Doc,树仍然不为空,那么继续遍历其左节点,此时指针指向Bashful,此时bashful是个孤立的树叶,但仍是一棵子树。继续遍历其左子树,发现此时为空,程序直接返回上一级指针,也就是指向Bashful,并执行输出操作。接下来遍历其右节点,同理发现没有,指针回退到Doc,由于是由inOrder(tree -> left);回退而来,所以接下来输出Doc,然后执行inOrder(tree -> right),指针指向Doc的右节点,也就是Dopey。过程同Bashful。完成后指针就会退至Grumpy。由于是由inOrder(tree -> left);回退而来,所以接下来输出Doc,然后执行inOrder(tree -> right),指针此时指向Sleepy,树为空吗,否,指针继续往下,遍历左节点,也就是指针此时指向Happy,此时Happy是个孤立的树叶,但仍是一棵子树。继续遍历其左子树,发现此时为空,程序直接返回上一级指针,也就是指向Happy,并执行输出操作。然后重复上述的操作,得出结果应该是这样的:
Bashful
Doc
Dopey
grumpy
Happy
Sleepy
Sneezy
后序遍历(postOrder)
步骤为:
- Go left
- Go right
- Do something
代码为:
void postOrder(Tree * tree) {
if(tree == NULL) return;
postOrder(tree -> left);
postOrder(tree -> right);
cout<< tree->value << endl;
}
首先,指针指向Grumpy,判断树是否为空,是则返回,否则继续。后序遍历树的左节点,此时指针指向Doc,树为空吗?否,指针继续指向左子树,也就是Bashful。树仍然不为空,继续往下,发现左右子树都为空,立刻返回,然后输出该节点的值,也就是Bashful。指针往上返回,指向Doc,然后执行postOrder(tree -> right);此时指针指向Dopey,同上操作,输出Dopey。随后指针回退,指向Grumpy,此时树为空吗?否,遍历左边,输出Doc,然后遍历右边,指针指向Sleepy,此时树为空吗?否,继续遍历Happy,为空吗?否,查看树的左节点,为空,立刻返回,输出Happy,随后指针返回Sleep中,查看右节点,为空吗?否,遍历左右子树,为空,立刻返回,然后输出Sneezy,指针回退到Grumpy中,输出Sleepy,最后输出根节点。因此最终的结果为:
Bashful
Dopey
Doc
Happy
Sneezy
Sleepy
Grumpy
层次遍历(levelOrder)
考虑这样一个问题,如果我们需要将树的节点按顺序从左往右,依次按顺序输出,我们应该怎么办呢?能否按上述的步骤进行递归实现呢?唔。。。似乎不太容易,但是按顺序依次输出似乎让我们想到了什么?我们前面介绍过一种顺序数据结构,没错,就是我们的栈跟队列,显然这种情况我们选用队列好多了。我们要做的步骤为:
1. 将树装进我们的队列中,如果队列未空执行下列操作
2. 将树根移出队列
3. 对节点进行操作
4. 如果有左节点,进行出队列操作
5. 如果有右队列,进行出队列操作
这样,出队列的顺序就是树的层次顺序。代码为:
void levelOrder(Tree *tree) {
Queue<Tree *>treeQueue; //建立一个队列存储树,类型为指向树的指针
treeQueue.enqueue(tree);//将树装进我们的队列中
while (!treeQueue.isEmpty()) { //判断树是否为空
Tree *node = treeQueue.dequeue(); //定义一个Tree类型的指针,指向出队的节点
cout << node->value << " "; //输出该节点的值
if (node->left != NULL) { //如果存在左节点,将左节点移除
treeQueue.enqueue(node->left);
}
if (node->right != NULL) { //如果存在右节点,将右节点移除
treeQueue.enqueue(node->right);
}
}
}
因此,按上述的例子,输出为:
Grumpy
Doc
Sleepy
Bashful
Dopey
Happy
Sneezy
好了,树的四种遍历方式就先讲到这里。不过个人感觉,再去做一下严蔚敏《数据结构(C语言版)》P129的那个表达式,能受益匪浅!!!