本文目录
- 一、树的遍历
- 二、遍历顺序
- 先序遍历
- 中序遍历
- 后序遍历
- 层序遍历
- 三、代码实现
系列目录
一、树的遍历
树的遍历:从根节点出发,按照某种顺序依次访问且只访问一次树中的所有节点。
从“树的遍历”的定义可知,有三个关键点:
- 从根节点出发;
- 访问树中的所有节点,且只访问一次;
- 遍历顺序:先序遍历、中序遍历、后序遍历和层序遍历;
补充:写这篇文章的目的是为了方便后面二叉查找树、AVL树、红黑树的实现验证做铺垫。后续相关二叉树文章中的实现代码都会通过本文中的工具类cn.wxy.blog.TraversalTreeTool来做验证。
二、遍历顺序
1.先序遍历
遍历流程:从根节点出发
- 首先,处理当前节点
- 然后,处理左子树
- 处理左子树的含义:处理完左孩子作为根节点的那颗子树上的所有节点
- 处理左子树的流程和本流程一致,相当于把左孩子看作当前节点
- 最后,处理右子树
- 处理右子树的含义:处理完右孩子作为根节点的那颗子树上的所有节点
- 处理右子树的流程和本流程一致,相当于把右孩子看作当前节点
如下示例,先序遍历的顺序:A B D G H C E I F
2.中序遍历
遍历流程:从根节点出发
- 首先,处理左子树
- 处理左子树的含义:处理完左孩子作为根节点的那颗子树上的所有节点
- 处理左子树的流程和本流程一致,相当于把左孩子看作当前节点
- 然后,处理当前节点
- 最后,处理右子树
- 处理右子树的含义:处理完右孩子作为根节点的那颗子树上的所有节点
- 处理右子树的流程和本流程一致,相当于把右孩子看作当前节点
如下示例,中序遍历的顺序:G D H B A E I G F
3.后序遍历
遍历流程:从根节点出发
- 首先,处理左子树
- 处理左子树的含义:处理完左孩子作为根节点的那颗子树上的所有节点
- 处理左子树的流程和本流程一致,相当于把左孩子看作当前节点
- 然后,处理右子树
- 处理右子树的含义:处理完右孩子作为根节点的那颗子树上的所有节点
- 处理右子树的流程和本流程一致,相当于把右孩子看作当前节点
- 最后,处理当前节点
如下示例,后序遍历的顺序:G H D B I E F C A
先序遍历、中序遍历和后序遍历的起点都是根节点,不同之处在于处理当前节点的时机:
- 先序遍历一开始就处理当前节点
- 中序遍历要处理完左子树之后才会处理当前节点
- 后序遍历要处理完左子树和右子树之后才会处理当前节点
三种遍历的差异见下图:
4.层序遍历
流程:从跟节点出发,逐层往下直到最后一层叶节点,一层一层的处理
如下示例,层序遍历的顺序:A B C D E F G H I
三、代码实现
先序、中序、后序、层序遍历的实现代码TraversalTreeTool类如下,其中静态内部类TreeNode接口定义了二叉树中节点的规范,后续的二叉排序树、AVL、红黑树、大顶堆小顶堆的节点都需要实现该规范,以方便TraversalTreeTool工具类对二叉树进行遍历,验证结果。
package cn.wxy.blog;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 二叉树的先序、中序、后序、层序遍历树
* @author 王大锤
* @date 2021年6月11日
*/
public class TraversalTreeTool {
/**
* 定义Node规范,方便TraversalTreeTool工具进行遍历(后面各种二叉树的节点都需要实现该规范)
* 后面key是Integer或String,所以这里简单实现不要求重写equals和hashcode方法
* @author 王大锤
* @date 2021年6月11日
*/
interface TreeNode<K, N extends TreeNode<K, N>> {
K getKey();
N getParent();
void setParent(N node);
N getLeftChild();
void setLeftChild(N node);
N getRightChild();
void setRightChild(N node);
}
/**
* 递归实现先序遍历
* @param root
*/
public static void preorderTraversalByRecursion(TreeNode<?, ?> root) {
if (Objects.isNull(root))
return;
System.out.print(root + " ");
preorderTraversalByRecursion(root.getLeftChild());
preorderTraversalByRecursion(root.getRightChild());
}
/**
* 递归实现中序遍历
* @param root
*/
public static void inorderTraversalByRecursion(TreeNode<?, ?> root) {
if (Objects.isNull(root))
return;
inorderTraversalByRecursion(root.getLeftChild());
System.out.print(root + " ");
inorderTraversalByRecursion(root.getRightChild());
}
/**
* 递归实现后序遍历
* @param root
*/
public static void postorderTraversalByRecursion(TreeNode<?, ?> root) {
if (Objects.isNull(root))
return;
postorderTraversalByRecursion(root.getLeftChild());
postorderTraversalByRecursion(root.getRightChild());
System.out.print(root + " ");
}
/**
* 层序遍历:把每一层的Node打印后加入List,不断重复
* @param root
*/
public static void levelTraversal(TreeNode<?, ?> root) {
if (Objects.isNull(root))
return;
List<? super TreeNode<?, ?>> list = new ArrayList<>();
list.add(root);
while (list.size() > 0) {
List<? super TreeNode<?, ?>> tmpList = new ArrayList<>();
list.stream().forEach(node -> {
TreeNode<?, ?> tmpNode = null;
if (node instanceof TreeNode)
tmpNode = (TreeNode<?, ?>) node;
System.out.print(tmpNode + " ");
if (Objects.nonNull(tmpNode.getLeftChild()))
tmpList.add(tmpNode.getLeftChild());
if (Objects.nonNull(tmpNode.getRightChild()))
tmpList.add(tmpNode.getRightChild());
});
list = tmpList;
}
}
}
为了验证代码实现的正确性,接着定义一颗二叉树BinaryTree。
BinaryTree逻辑结构是一颗二叉树,物理实现上采用链式存储,所以要先定义树的节点Node implements TreeNode>:
- TreeNode:在cn.wxy.blog.TraversalTreeTool中定义的内部类,用来定义所有二叉树的节点需要实现的规范,以方便TraversalTreeTool工具类对树进行遍历;
- Node:在BinaryTree中定义的内部类,用于具体实现二叉树的节点,实现TreeNode规范
为了维护二叉树的逻辑结构,每个二叉树节点有parent、leftChild、rightChild三个属性。
定义了二叉树结点之后,二叉树的抽象模型中只需要持有根节点的引用就能掌握整棵树,具体代码实现如下:
package cn.wxy.blog;
import java.util.Objects;
import cn.wxy.blog.TraversalTreeTool.TreeNode;
/**
* 定义一颗二叉树,只有一个属性,即根节点
* @author 王大锤
* @date 2021年6月11日
*/
public class BinaryTree<K> {
/**
* 二叉树中节点的具体实现
* @author 王大锤
* @date 2021年6月11日
*/
static class Node<K> implements TreeNode<K, Node<K>> {
private K key;
private Node<K> parent;
private Node<K> leftChild;
private Node<K> rightChild;
public Node(K key) {
this.key = key;
}
@Override
public K getKey() {
return this.key;
}
@Override
public Node<K> getLeftChild() {
return this.leftChild;
}
@Override
public Node<K> getRightChild() {
return this.rightChild;
}
@Override
public void setLeftChild(Node<K> node) {
this.leftChild = node;
}
@Override
public void setRightChild(Node<K> node) {
this.rightChild = node;
}
@Override
public Node<K> getParent() {
return this.parent;
}
@Override
public void setParent(Node<K> node) {
this.parent = node;
}
@Override
public String toString() {
return this.key + " ";
}
}
private Node<K> root;
public Node<K> getRoot() {
return this.root;
}
public Node<K> setRoot(Node<K> node) {
if (Objects.nonNull(this.root) && Objects.nonNull(node)) {
// 处理左孩子
node.leftChild = this.root.leftChild;
if (Objects.nonNull(this.root.leftChild))
this.root.leftChild.parent = node;
this.root.leftChild = null;
// 处理右孩子
node.rightChild = this.root.rightChild;
if (Objects.nonNull(this.root.rightChild))
this.root.rightChild.parent = node;
this.root.rightChild = null;
}
this.root = node;
return this.root;
}
}
最后,写个main方法,组装如第二部分图示的一棵二叉树,调用遍历方法打印节点:
package cn.wxy.blog;
import cn.wxy.blog.BinaryTree.Node;
public class Main {
public static void main(String[] args) {
Node<String> A = new Node<String>("A");
Node<String> B = new Node<String>("B");
Node<String> C = new Node<String>("C");
Node<String> D = new Node<String>("D");
Node<String> E = new Node<String>("E");
Node<String> F = new Node<String>("F");
Node<String> G = new Node<String>("G");
Node<String> H = new Node<String>("H");
Node<String> I = new Node<String>("I");
BinaryTree<String> binaryTree = new BinaryTree<String>();
binaryTree.setRoot(A);
A.setLeftChild(B);
A.setRightChild(C);
B.setParent(A);
B.setLeftChild(D);
C.setParent(A);
C.setLeftChild(E);
C.setRightChild(F);
D.setParent(B);
D.setLeftChild(G);
D.setRightChild(H);
E.setParent(C);
E.setRightChild(I);
G.setParent(D);
H.setParent(D);
I.setParent(E);
System.out.print("先序遍历:");
TraversalTreeTool.preorderTraversalByRecursion(binaryTree.getRoot());
System.out.println();
System.out.print("中序遍历:");
TraversalTreeTool.inorderTraversalByRecursion(binaryTree.getRoot());
System.out.println();
System.out.print("后序遍历:");
TraversalTreeTool.postorderTraversalByRecursion(binaryTree.getRoot());
System.out.println();
System.out.print("层序遍历:");
TraversalTreeTool.levelTraversal(binaryTree.getRoot());
}
}
输出结果:
先序遍历:A B D G H C E I F
中序遍历:G D H B A E I C F
后序遍历:G H D B I E F C A
层序遍历:A B C D E F G H I
本例中二叉树的parent属性看似没用,但实际上后续各种排序树在做节点结构变化的时候都需要这个属性提供支持,故而保留。
参考资料:
- 《大话数据结构》
- 《数据结构与算法分析:Java语言描述》
- 《算法导论》