十二、二叉树的遍历
简介
-
遍历是数据结构中的常见操作
把所有元素都访问一遍 -
线性数据结构的遍历比较简单
正序遍历
逆序遍历 -
根据节点访问顺序的不同,二叉树的常见遍历方式有4种
前序遍历(Preorder Traversal)
中序遍历(Inorder Traversal)
后序遍历(Postorder Traversal)
层序遍历(Level Order Traversal)
前序遍历(Preorder Traversal)
前序遍历(Preorder Traversal)访问顺序: 、前序遍历、前序遍历
前序遍历 – 递归
public void preorderTraversal() {
preorderTraversal(root);
}
private void preorderTraversal(Node<E> node) {
if(node == null) return;
System.out.println(node.element);
preorderTraversal(node.left);
preorderTraversal(node.right);
}
前序遍历 – 非递归
前序遍历 – 非递归public void preorder(Visitor<E> visitor) {
if(visitor == null || root == null) return;
Node<E> node = root;
Stack<Node<E>> stack = new Stack<>();
while(true) {
if(node != null) {
if(visitor.visit(node.element)) return;
// 将右子节点入栈
if(node.right != null) {
stack.push(node.right);
}
// 向左走
node = node.left;
}else if(stack.isEmpty()) {
return;
}else {
node = stack.pop();
}
}
}
非递归前序遍历的另一种思路
public void preorder(Visitor<E> visitor) {
if(visitor == null || root == null) return;
Stack<Node<E>> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
Node<E> node = stack.pop();
if(visitor.visit(node.element)) return;
if(node.right != null) {
stack.push(node.right);
}
if(node.left != null) {
stack.push(node.left);
}
}
}
中序遍历(Inorder Traversal)
中序遍历(Inorder Traversal)访问顺序: 中序遍历、、中序遍历
访问顺序:中序遍历、、中序遍历
二叉搜索树的中序遍历特点:
二叉搜索树的中序遍历结果是升序或者降序的
中序遍历 – 递归
public void inorderTraversal() {
inorderTraversal(root);
}
private void inorderTraversal(Node<E> node) {
if(node == null) return;
inorderTraversal(node.left);
System.out.println(node.element);
inorderTraversal(node.right);
}
中序遍历 – 非递归
中序遍历 – 非递归public void inorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Node<E> node = root;
Stack<Node<E>> stack = new Stack<>();
while (true) {
if (node != null) {
stack.push(node);
// 向左走
node = node.left;
} else if (stack.isEmpty()) {
return;
} else {
node = stack.pop();
// 访问node节点
if (visitor.visit(node.element)) return;
// 让右节点进行中序遍历
node = node.right;
}
}
}
后序遍历(Postorder Traversal)
后序遍历(Postorder Traversal)访问顺序: 后序遍历、后序遍历、
后序遍历 – 递归
public void postorderTraversal() {
postorderTraversal(root);
}
private void postorderTraversal(Node<E> node) {
if(node == null) return;
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.println(node.element);
}
后序遍历 – 非递归
后序遍历 – 非递归public void postorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Stack<Node<E>> stack = new Stack<>();
stack.push(root);
Node<E> prev = null;// 记录上一次弹出访问的节点
while(!stack.isEmpty()) {
Node<E> top =stack.peek();
if(top.isLeaf() || (prev != null && prev.parent == top) ) {
prev = stack.pop();
if (visitor.visit(prev.element)) return;
}else {
if(top.right != null) {
stack.push(top.right);
}
if(top.left != null) {
stack.push(top.left);
}
}
}
}
public void postorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Stack<E> resStack = new Stack<>();
Stack<Node<E>> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
Node<E> node = stack.pop();
resStack.push(node.element);
if(node.left != null) {
stack.push(node.left);
}
if(node.right != null) {
stack.push(node.right);
}
}
while (!resStack.isEmpty()) {
E element = resStack.pop();
if (visitor.visit(element)) return;
}
}
层序遍历(Level Order Traversal)
层序遍历(Level Order Traversal)访问顺序: 从上到下、从左到右依次访问每一个节点
实现思路:使用队列
- 将根节点入队
- 循环执行以下操作,直到队列为空
- 将队头节点 A 出队,进行访问
- 将 A 的左子节点入队
- 将 A 的右子节点入队
public void levelOrderTraversal() {
if(root == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
Node<E> node = queue.poll();
System.out.println(node.element);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
}
设计遍历接口
思考
前面虽然写了前中后层序遍历,但是这写遍历,只是打印了节点数据,那么外界如何访问到这个节点数据呢?
如果允许外界遍历二叉树的元素?你会如何设计接口?
定义Visitor接口
public static interface Visitor<E> {
void visit(E element);
}
修改前序遍历
public void preorder(Visitor<E> visitor) {
preorder(root,visitor);
}
private void preorder(Node<E> node,Visitor<E> visitor) {
if(node == null || visitor == null) return;
visitor.visit(node.element);
preorder(node.left,visitor);
preorder(node.right,visitor);
}
修改中序遍历
public void inorder(Visitor<E> visitor) {
inorder(root,visitor);
}
private void inorder(Node<E> node,Visitor<E> visitor) {
if(node == null || visitor == null) return;
inorder(node.left,visitor);
visitor.visit(node.element);
inorder(node.right,visitor);
}
修改后序遍历
public void postorder(Visitor<E> visitor) {
postorder(root,visitor);
}
private void postorder(Node<E> node,Visitor<E> visitor) {
if(node == null || visitor == null) return;
postorder(node.left,visitor);
postorder(node.right,visitor);
visitor.visit(node.element);
}
修改层序遍历
public void levelOrder(Visitor<E> visitor) {
if(root == null || visitor == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
Node<E> node = queue.poll();
visitor.visit(node.element);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
}
使用
bst.preorder(new Visitor<Integer>() {
@Override
public void visit(Integer element) {
System.out.print(element+" ");
}
});
增强遍历接口
现在提供了给外界遍历的接口,但是如果遍历的中间想停掉遍历,该怎么处理呢?
修改Visitor接口里的方法和层序遍历
但是这样修改了Visitor接口后,层序遍历可以完美的兼容,那么前、中、后序遍历就没法兼容了。
这里拿中序遍历来举例:
如果visitor.visit(node.element);
的返回值用一个类似全局变量的记录一下,下次遍历的时候就可以根据上次的进行判断,但是如果直接使用全局遍历的话,在多线程操作下就会有问题。所以这里可以在Visitor
里面新增一个变量进行使用就可以。
遍历的应用
-
前序遍历
树状结构展示(注意左右子树的顺序) -
中序遍历
二叉搜索树的中序遍历按升序或者降序处理节点 -
后序遍历
适用于一些先子后父的操作 -
层序遍历
计算二叉树的高度
判断一棵树是否为完全二叉树
根据遍历结果重构二叉树
举例:
计算二叉树的高度
二叉树的高度其实就是根结点的高度。什么叫做高度呢?从当前结点开始一直到最远的叶子结点,所经历的结点数量就是高度。
递归方法
计算某个结点的高度其实就是看它左右结点的高度的最大值。
public int height() {
return height(root);
}
private int height(Node<E> node) {
if(node == null) return 0;
return 1 + Math.max(height(node.left),height(node.right));
}
非递归方法
使用非递归的方法,就是迭代实现,可以用层序遍历来实现。
这里使用层序遍历,当每访问一层就使局部变量height++,那我们怎么知道什么时候访问完一层呢?通过层序遍历的特点,可以知道每访问完一层,就可以通过队列知道下一层的结点数量。因此可以定义局部变量levelSize来表示每一层的结点数量。
public int height() {
if(root == null) return 0;
//树的高度
int height = 0;
// 存储着每一层的元素数量
int levelSize = 1;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
Node<E> node = queue.poll();
levelSize--;//levelSize减为0,代表这一层就访问完了。
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
if(levelSize == 0) {//意味着即将要访问下一层
levelSize = queue.size();
height++;
}
}
return height;
}
判断一棵树是否为完全二叉树
- 如果树为空,返回 false
- 如果树不为空,开始层序遍历二叉树(用队列)
- 如果
node.left != null && node.right != null
,将node.left、node.right
按顺序入队 - 如果
node.left == null && node.right != null
,返回false
- 如果
node.right!=null && node.right == null
或者node.left == null && node.right != null
- 那么后面遍历的节点应该都为叶子节点,才是完全二叉树
- 否则返回
false
- 遍历结束,返回
true
- 如果
这里为了方便实现,需要在Node结点类中新增加是否是叶子结点isLeaf方法,和是否有左右两个结点hasTwoChildren方法
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;
}
// 是否是叶子结点
public boolean isLeaf() {
return left == null && right == null;
}
// 是否有左右两个结点
public boolean hasTwoChildren() {
return left != null && right != null;
}
}
public boolean isComplete() {
if(root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while(!queue.isEmpty()) {
Node<E> node = queue.poll();
if(leaf && !node.isLeaf()) {
//如果要求你是叶子结点,但是你不是叶子结点
return false;
}
if(node.hasTwoChildren()) {
queue.offer(node.left);
queue.offer(node.right);
}else if (node.left == null && node.right != null) {
return false;
}else {// 后面遍历的结点都必须是叶子结点
leaf = true;
if(node.left != null) {
queue.offer(node.left);
}
}
}
return true;
}
下面我们简化代码逻辑
- 如果树为空,返回 false
- 如果树不为空,开始层序遍历二叉树(用队列)
- 如果
node.left!=null
,将node.left
入队 - 如果
node.left==null && node.right!=null
,返回false
- 如果
node.right!=null
,将node.right
入队 - 如果
node.right==null
- 那么后面遍历的节点应该都为叶子节点,才是完全二叉树
- 否则返回
false
- 遍历结束,返回
true
- 如果
public boolean isComplete() {
if(root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while(!queue.isEmpty()) {
Node<E> node = queue.poll();
if(leaf && !node.isLeaf()) {
//如果要求你是叶子结点,但是你不是叶子结点
return false;
}
if(node.left != null) {
queue.offer(node.left);
}else if(node.right != null) {
// node.left == null && node.right != null
return false;
}
if(node.right != null) {
queue.offer(node.right);
}else {// node.right == null
leaf = true;
}
}
return true;
}
226. 翻转二叉树
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
方法一:递归
前序遍历
public TreeNode invertTreePreorder(TreeNode root) {
// 采用前序遍历
if(root == null) return root;
// 交换一下左右
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTreePreorder(root.left);// 遍历左
invertTreePreorder(root.right);// 遍历右
return root;
}
中序遍历
public TreeNode invertTreeInorder(TreeNode root) {
// 采用中序遍历
if(root == null) return root;
invertTreeInorder(root.left);
// 交换一下左右
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTreeInorder(root.left);// 注意这里不是right了,
return root;
}
后序遍历
public TreeNode invertTreePostorder(TreeNode root) {
// 采用后序遍历
if(root == null) return root;
invertTreePostorder(root.left);// 遍历左
invertTreePostorder(root.right);// 遍历右
// 交换一下左右
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
return root;
}
方法二:迭代
层序遍历
public TreeNode invertTreeLevelOrder(TreeNode root) {
// 采用层序遍历
if(root == null) return root;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
// 交换一下左右
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
return root;
}