十二、二叉树的遍历

127 阅读6分钟

简介

  • 遍历是数据结构中的常见操作 把所有元素都访问一遍

  • 线性数据结构的遍历比较简单 正序遍历 逆序遍历

  • 根据节点访问顺序的不同,二叉树的常见遍历方式有4种 前序遍历(Preorder Traversal) 中序遍历(Inorder Traversal) 后序遍历(Postorder Traversal) 层序遍历(Level Order Traversal)

前序遍历(Preorder Traversal)

前序遍历(Preorder Traversal) 访问顺序: 根节点\color{#ed7d30}{根节点}、前序遍历左子树\color{#00afef}{左子树}、前序遍历右子树\color{#00b050}{右子树} 74213598111012\color{#ed7d30}{7}、\color{#00afef}{4、2、1、3、5}、\color{#00b050}{9、8、11、10、12}

前序遍历 – 递归

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) 访问顺序: 中序遍历左子树\color{#00afef}{左子树}根节点\color{#ed7d30}{根节点}、中序遍历右子树\color{#00b050}{右子树} 12345789101112\color{#00afef}{1、2、3、4、5}、\color{#ed7d30}{7}、\color{#00b050}{8、9、10、11、12}

如果访问顺序是下面这样呢?\color{red}{如果访问顺序是下面这样呢?} 访问顺序:中序遍历右子树\color{#00b050}{右子树}根节点\color{#ed7d30}{根节点}、中序遍历左子树\color{#00afef}{左子树} 12111098754321\color{#00b050}{12、11、10、9、8 }、\color{#ed7d30}{7}、\color{#00afef}{5、4、3、2、1}

二叉搜索树的中序遍历特点: 二叉搜索树的中序遍历结果是升序或者降序的

中序遍历 – 递归

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)

访问顺序: 后序遍历左子树\color{#00afef}{左子树}、后序遍历右子树\color{#00b050}{右子树}根节点\color{#ed7d30}{根节点} 13254810121197\color{#00afef}{1、3、2、5、4}、\color{#00b050}{8、10、12、11、9}、\color{#ed7d30}{7}

后序遍历 – 递归

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) 访问顺序: 从上到下、从左到右依次访问每一个节点 74925811131012\color{#ed7d30}{7}、\color{#00afef}{4、9}、\color{#00b050}{2、5、8、11}、1、3、10、12

实现思路:使用队列

  1. 将根节点入队
  2. 循环执行以下操作,直到队列为空
    • 将队头节点 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;
}