树从存储方式上可分为顺序树和链式树
这节我们主要说的是链式树。
1. 定义结点
public
2. 树的创建
定义一个Tree类,里面主要实现树的创建,和一些遍历操作,先部分展示代码并讲解,在最后会给出全部源码。
我们为了养成良好的习惯,建议把每个东西归结成一个类,比如树,栈,队列,都分开定义,到用的时候,直接new就行了。
先定义数据结构
1.树的结构:
public
2.栈的结构:(树的非递归遍历会用到)
public
3.循环队列的结构:(树的层序遍历会用到)
public
4. 树的递归创建方式:(使用先序创建)
// 递归创建树(树的头节点)
3. 树的递归遍历
我们利用三种遍历的特点,根据他们的打印顺序总结出来:
先序:根左右
中序:左跟右
后序:左右跟
3.1 树的递归先序遍历
// 先序遍历(递归)
3.2 树的递归中序遍历
// 中序遍历(递归)
3.3 树的递归后序遍历
// 后序遍历(递归)
递归的遍历特别简单,只需要知道递归的原理就OK了,不做详解。
4. 树的非递归遍历
树的非递归遍历,还会用到栈,利用栈的先进后出的特点,实现树的遍历。
4.1 树的非递归先序遍历
这块我会给出两种方法,原理基本相同。
(1)根据先序打印特点:根左右。
我们用到压栈和出栈来模拟这个过程:
- 由于栈是先进后出的,所以我们先访问根节点,压栈。
- 然后判断栈不为空,然后出栈,打印。
- 对出栈的结点进行判断,他有没有右孩子,如果有,把右孩子压栈。
- 再判断有没有左孩子,如果有,把左孩子压栈。
- 完成后,再跳回第2步,一直循环
// 先序遍历(非递归)
(2)第二种只是把压栈的顺序,进行了一点修正,其他的都差不多,但是,为了方便记忆非递归的先序,中序,后序,建议学习第二种的算法。
// 先序遍历(非递归)
void nonPreOrder2(Node tree) {
if (tree == null) {
System.out.println("树不存在");
return;
}
Node temp = null;
temp = tree;
while (!stack.IsEmpty() || temp != null) {
while (temp != null) {
System.out.println(temp.name);
stack.push(temp);
temp = temp.lchild;
}
temp = stack.pop();
temp = temp.rchild;
}
}
4.2 树的非递归中序遍历
根据中序打印特点:左跟右。
我们用到压栈和出栈来模拟这个过程:
- 由于栈是先进后出的,所以我们先访问根节点,用temp结点记录。
- 然后判断栈不为空或者temp不为空,进入循环。
- 先将temp的所有左子树全部压栈。
- 然后出栈栈顶结点,打印
- 然后再判断出栈的这个结点有没有右孩子,如果有,将右孩子入栈
- 再将右孩子结点 的所有左孩子全部入栈,即从第3步开始循环。
// 中序遍历(非递归)
中序的非递归遍历,和上述的先序的第二种非递归遍历比较,会发现,只改变了打印的位置,其他的都没有变化,所以建议记住,先序的第二种算法。
4.2 树的非递归后序遍历
后序我会给出,自己总结的三种方法,各有特点,其中第一种可以和前两对应起来,改动较小,对应编程能力差的同学,可以着重学习,后两种方法也很nice,想要多学点的同学,也可以学习。
(1)第一种:根据后序打印特点:左右跟。
我们根据先序的过程,先访问根节点,再访问左孩子,最后访问右孩子,根据具体的顺序,我们可以发现其实后序只需要,把先序打印的过程,换成一个如另一个栈的过程,所以我们需要两个栈,再把先将左孩子压栈,后将右孩子压栈,变成先将右孩子压栈,再将左孩子压栈,最后我们的第二个栈中就得到了后序的倒序,我们再将第二个栈出栈,打印就行。
// 后序遍历(非递归3)
后两种方法,都需要明白一个特点:
我们可以观察前序、中序、后序的过程,根据他们是第几次访问根节点然后再打印的特点,我们可以发现:
- 先序都是先访问根节点,然后打印,然后再访问他的左孩子,把他的左孩子又看出根节点,然后打印,又去访问他的左孩子,如果左孩子不存在,去访问他的右孩子,把他的右孩子又看成根节点,一直重复上面的步骤。(这块的根节点指的是每一个结点都看成根节点,不是特指树的根节点)。
- 中序都是先访问根节点,然后访问所有的左孩子,左孩子全部访问结束,再回来访问根节点,打印根节点,再去访问右孩子,又去访问右孩子的所有左孩子,一直重复上面步骤。
- 后序都是先访问根节点,再访问他的全部左孩子,左孩子全部访问结束,再回来访问他的根节点,又访问他的全部右孩子,右孩子全部访问结束,再反过来打印根节点。(这块的根节点指的是每一个结点都看成根节点,不是特指树的根节点)。
总结一个特点:
- 先序:第一次访问一个结点时打印
- 中序:第二次访问一个结点时打印
- 后序:第三次访问一个结点时打印
后序什么时候打印,可以根据他是不是访问完右孩子过来的,如果是访问完右孩子过来的,就让他打印,否则去遍历他的右孩子。
下面的两种方法都是根据,这个特点完成的,这是有点稍微的区别。
(2)第二种:多创建一个结点,用来记录出栈的元素,进行判断
// 后序遍历(非递归1)
(3)第三种:利用在结点中,多一个属性,用来记录是第几次访问
// 后序遍历(非递归2)
5. 树的层序遍历
用到了队列
// 层序遍历
6. 计算树的深度
// 递归计算树的深度
7. 反转二叉树
7.1 递归方法
// 反转二叉树(递归)
7.2 非递归算法
用到栈
// 反转二叉树(非递归)
8. 统计结点数(递归)
// 统计结点数(递归)
9. 统计叶子结点数(递归)
//统计叶子结点数(递归)
10. 判断一个结点是否是一个子树上的结点(递归)
//找一个结点是否是一个子树的结点
11. 代码的综合
对上面讲述所有代码算法的综合
树的创建和操作:
//class Tree
import
栈的结构:
//class Seqstack
public
循环队列的结构:
//class Seqqueue
public
给个测试用例:
//class Client
public