二叉树遍历概述
遍历是数据结构中的常见操作。
二叉树的遍历就是把所有元素都访问一遍。
根据节点访问顺序的不同,二叉树的常见遍历方式有4种:
- 前序遍历(Preorder Traversal)
- 中序遍历(Inorder Traversal)
- 后序遍历(Postorder Traversal)
- 层序遍历(Level Order Traversal)
先序遍历
先序遍历的思路:
1.访问根节点;
2.访问当前节点的左子树;
3.若当前节点无左子树,则访问当前节点的右子树。
对于左子树和右子树,同样采用先序遍历。
二叉树先序遍历示意图:
上图二叉树先序遍历的过程:
1)访问该二叉树的根节点,找到 A;
2)访问节点 A 的左子树,找到节点 B;
3)访问节点 B 的左子树,找到节点 D;
4)由于访问节点 D 左子树失败,且也没有右子树,因此以节点 D 为根节点的子树遍历完成。但节点 B 还没有遍历其右子树,因此现在开始遍历,即访问节点 E;
5)由于节点 E 无左右子树,因此节点 E 遍历完成,并且由此以节点 B 为根节点的子树也遍历完成。现在回到节点 A ,并开始遍历该节点的右子树,即访问节点 C;
6)访问节点 C 左子树,找到节点 F;
7)由于节点 F 无左右子树,因此节点 F 遍历完成,回到节点 C 并遍历其右子树,找到节点 G;
8)节点 G 无左右子树,因此以节点 C 为根节点的子树遍历完成,同时回归节点 A。整个二叉树遍历完成。
先序遍历结果:
A、B、D、E、C、F、G
先序遍历递归实现
注意:对于节点类,后面三种遍历方式使用相同节点类。
节点类:
/**
* 节点类
* @param <E> 泛型
*/
private static class Node<E> {
E element; //节点数据
Node<E> left; //左节点
Node<E> right; //右节点
Node<E> parent; //父节点 - 额外增加
public Node(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
}
先序遍历递归代码:
/**
* 先序遍历对外 api
*/
public void preOrder() {
preOrderPrint(root);
}
/**
* 先序遍历具体实现 - 递归
*/
private void preOrderPrint(Node<E> node) {
if (node == null) return;
// 访问子树节点数据
System.out.println(node.element);
// 递归遍历左子树
preOrderPrint(node.left);
// 递归遍历右子树
preOrderPrint(node.right);
}
先序遍历非递归实现(栈结构)
先序遍历的非递归借助栈结构完成:
①将根节点入栈;
②判栈空,获取栈顶元素输出;
③判断右子树是否为空,不为空则右子树入栈;再判断左子树是否为空,不为空则左子树入栈在,回至②执行。
③中必须是先判断右子树,再判断左子树,因为栈的规则是先进后出,即:右子树先入栈,左子树后入栈,访问节点数据(出栈)时,则会先左子树再右子树。
对于上图二叉树,遍历过程大致如下:
第一步:根节点(A)入栈,此时栈中只有节点 A。
第二步:栈不为空,访问栈顶元素(A),出栈(节点A出栈),然后先后判断 A 的右、左子树,并入栈,此时栈中有节点B(栈顶)和节点C(栈底)。
第三步:重复执行(判断栈是否为空,不为空出栈),访问栈顶元素(B),出栈(节点B出栈),然后将 B 的右子树和左子树先后入栈,此时栈中有节点D、E、C。
第四步,重复执行....
最后遍历的结果为:A、B、D、E、C、F、G
先序遍历非递归代码:
/**
* 先序遍历具体实现 - 非递归
* 1. 先将根节点入栈,
* 2. 当栈不为空时循环执行以下操作:
* 2.1 访问栈顶节点
* 2.2 栈顶节点的右节点入栈
* 2.3 栈顶节点的左节点入栈
*/
private void preOrderPrint2() {
if (root == null) return;
Stack<Node<E>> stack = new Stack<>();
// 1.根节点入栈
stack.push(root);
// 栈不为空时重复执行以下操作
while (!stack.isEmpty()) {
// 2.访问栈顶节点(出栈)
Node<E> topNode = stack.pop();
System.out.println(topNode.element);
// 3.栈顶节点右节点入栈(右节点不为空时)
if (topNode.right != null) {
stack.push(topNode.right);
}
// 4.栈顶节点左节点入栈(左节点不为空时)
if (topNode.left != null) {
stack.push(topNode.left);
}
}
}
中序遍历
中序遍历的思路:
1.访问当前节点的左子树;
2.访问根节点;
3.访问当前节点的右子树。
中序遍历左子树、根节点、中序遍历右子树。
二叉树中序遍历示意图:
上图采用中序遍历的过程为:
1)访问该二叉树的根节点,找到 A;
2)遍历节点 A 的左子树,找到节点 B;
3)遍历节点 B 的左子树,找到节点 D;
4)由于节点 D 无左孩子,因此找到节点 D,并遍历节点 D 的右子树;
5)由于节点 D 无右子树,因此节点 B 的左子树遍历完成,访问节点 B;
6)遍历节点 B 的右子树,找到节点 E;
7)由于节点 E 无左子树,因此访问节点 E ,又因为节点 E 没有右子树,因此节点 A 的左子树遍历完成,访问节点 A ,并遍历节点 A 的右子树,找到节点 C;
8)遍历节点 C 的左子树,找到节点 F;
9)由于节点 F 无左子树,因此访问节点 F,又因为该节点无右子树,因此节点 C 的左子树遍历完成,开始访问节点 C ,并遍历节点 C 的右子树,找到节点 G;
10)由于节点 G 无左子树,因此访问节点 G,又因为该节点无右子树,因此节点 A 的右子树遍历完成,即整棵树遍历完成;
因此,采用中序遍历的结果为:
D、B、E、A、F、C、G
中序遍历递归实现
中序遍历代码:
public void inOrder() {
inOrderPrint(root);
}
/**
* 中序遍历具体实现
*/
public void inOrderPrint(Node<E> node) {
if (node == null) return;
// 递归遍历左子树
inOrderPrint(node.left);
// 访问子树节点数据
System.out.println(node.element);
// 递归遍历右子树
inOrderPrint(node.right);
}
中序遍历非递归实现(栈结构)
/**
* 中序遍历具体实现 - 非递归,利用栈实现
* 1.先将根节点入栈,设置 node = root;
* 2.将当前节点的所有左孩子入栈,直到左孩子为空
* 3.访问栈顶元素,如果栈顶元素存在右孩子,则继续第2步(栈顶元素右子树入栈)
* 4.重复第2、3步,直到栈为空并且所有的节点都被访问(循环结束)
*
* 上面四步分解为代码理解如下:
* 1.设置 node = root;
* 2.节点不为null 或者 栈不为空,则循环执行以下操作:
* 如果 node != null,将 node 入栈,设置 node = node.left;
* 如果 node == null,弹栈,访问栈顶元素,并设置 node = node.right;
*/
public void inorderPrint2() {
if (root == null) return;
Stack<Node<E>> stack = new Stack<>();
// 设置 node = root,在 while 循环一开始就会将根节点入栈
Node<E> node = root;
while (node != null || !stack.isEmpty()) {
// 将当前节点所有左子树入栈(最开始会把根节点入栈(node = root))
while (node != null) {
stack.push(node);
node = node.left;
}
// 弹栈访问栈顶元素
Node<E> headNode = stack.pop();
System.out.println(headNode.element);
// 将栈顶元素的右节点赋值给 node,执行栈顶元素右孩子入栈
node = headNode.right;
}
}
后序遍历
中序遍历的思路:
从根节点出发,依次遍历各节点的左右子树,直到当前节点左右子树遍历完成后,才访问该节点元素。
后序遍历左子树、后序遍历右子树、根节点
二叉树后序遍历示意图:
对此二叉树进行后序遍历的操作过程为:
1)从根节点 A 开始,遍历该节点的左子树(以节点 B 为根节点);
2)遍历节点 B 的左子树(以节点 D 为根节点);
3)由于节点 D 既没有左子树,也没有右子树,此时访问该节点中的元素 D,并回退到节点 B ,遍历节点 B 的右子树(以 E 为根节点);
4)由于节点 E 无左右子树,因此可以访问节点 E ,并且此时节点 B 的左右子树也遍历完成,因此也可以访问节点 B;
5)此时回退到节点 A ,开始遍历节点 A 的右子树(以节点 C 为根节点);
6)遍历节点 C 的左子树(以节点 F 为根节点);
7)由于节点 F 无左右子树,因此访问节点 F,并回退到节点 C,开始遍历节点 C 的右子树(以节点 G 为根节点);
8)由于节点 G 无左右子树,因此访问节点 G,并且节点 C 的左右子树也遍历完成,可以访问节点 C;节点 A 的左右子树也遍历完成,可以访问节点 A;
9)到此,整棵树的遍历结束。
因此,采用后序遍历的结果:
D、E、B、F、G、C、A
后序遍历递归实现
后序遍历代码:
public void postorder(){
postorderPrint(root);
}
/**
* 后序遍历具体实现 - 递归
*/
public void postorderPrint(Node<E> node) {
if (node == null) return;
postorderPrint(root.left);
postorderPrint(root.right);
System.out.println(node.element);
}
层序遍历
按照二叉树中的层次从左到右依次遍历每层中的结点。
从上到下、从左到右依次访问每一个节点
二叉树层序遍历示意图:
实现思路:使用队列
通过使用队列的数据结构,从树的根结点开始,依次将其左孩子和右孩子入队。而后每次队列中一个结点出队,都将其左孩子和右孩子入队,直到树中所有结点都出队,出队结点的先后顺序就是层次遍历的最终结果。
-
将根节点 A 入队
-
循环执行以下操作,直到队列为空:
将队头节点 X 出队,进行访问;
将 X 的左子节点入队;
将 X 的右子节点入队;
以上图为示:
首先,根结点 A 入队;
根结点 A 出队,出队的同时,将左孩子 B 和右孩子 C 分别入队;
队头结点 B 出队,出队的同时,将结点 B 的左孩子 D 和右孩子 E 依次入队;
队头结点 C 出队,出队的同时,将结点 C 的左孩子 F 和右孩子 G 依次入队;
不断地循环,直至队列内为空。
层序遍历代码:
/**
* 层序遍历
*/
public void levelOrder() {
if (root == null) return;;
Queue<Node<E>> queue = new LinkedList<>();
// 根节点入队
queue.offer(root);
while (!queue.isEmpty()) {
// 队列头结点出队
Node<E> headNode = queue.poll();
// 访问出队的头结点数据
System.out.println(headNode.element);
// 将队头结点的左右孩子依次入队
if (headNode.left != null) {
queue.offer(headNode.left);
}
if (headNode.right != null) {
queue.offer(headNode.right);
}
}
}