漫画算法-小灰的算法之旅(08)
1. 二叉树的遍历
1.1 为什么要研究遍历
二叉树的遍历有什么特殊之处?
在计算机程序中,遍历本身是一个线性操作。所以遍历同样具有线性结构的数组、链表是一件很容易的事情。反观二叉树,是典型的非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列,以不同的方式来遍历,遍历出的序列顺序也不同。
二叉树的遍历方式有哪些?
从节点之间的位置关系的角度来看,二叉树的遍历分为4种。
- 前序遍历
- 中序遍历
- 后序遍历
- 层序遍历
从更宏观的角度来看 ,二叉树的遍历归结为两大类。
- 深度优先遍历(前序遍历、中序遍历、后序遍历)
- 广度优先遍历(层序遍历)
1.2 深度优先遍历
深度优先和广度优先这两个概念不止局限与二叉树,它们更是一种抽象的算法思想,决定了访问某些复杂数据结构的顺序。在访问树、图等其他一些复杂数据结构时,这两个概念常常被使用到。
所谓深度优先:就是偏向于纵深,“一头扎到底”的访问方式。可能这种说法有些抽象。下面通过二叉树的前序遍历、中序遍历、后序遍历来理解深度优先到底是怎么回事。
前序遍历(根左右)
二叉树的前序遍历,输出顺序是根节点、左子树,右子树
上图就是一个二叉树的前序遍历,每个节点左侧的序号代表该节点的输出顺序,详细步骤如下。
- 首先输出的是根节点1.
- 由于根节点1存在左孩子,因此输出左孩子节点2.
- 由于节点2也存在左孩子,输出左孩子节点4.
- 节点4既没有左孩子,也没有右孩子,那么回到节点2,输出节点2点右孩子节点5.
- 节点5既没有左孩子,也没有右孩子,那么回到节点1,输出节点1点右孩子节点3.
- 节点3没有左孩子,但右右孩子,因此输出节点3点右孩子节点6.
至此,所有的节点都遍历输出完毕。
中序遍历(左根右)
二叉树的中序遍历,输出顺序是左子树,根节点,右子树。
上图就是一个二叉树的中序遍历,每个节点左侧的序号代表该节点的输出顺序,详细步骤如下。
- 首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子的节点,并输出该节点。显然,第一个没有左孩子的节点是节点4.
- 依照中序遍历的次序,接下来输出节点4的父节点2.
- 再输出节点2的有孩子节点5
- 以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的根节点1.
- 由于节点3没有左孩子,所以直接输出根节点1的右孩子节点3.
- 最后输出节点3的右孩子节点6.
至此,所有的节点都遍历输出完毕。
后序遍历(左右根)
二叉树的后序遍历,输出顺序是左子树,右子树,根节点。
上图就是一个二叉树的后序遍历,每一个节点左侧的序号代表该节点的输出顺序。详细步骤如下。
- 首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子的节点,并输出该节点。显然第一个没有左孩子的节点是节点4.
- 依照后序遍历的次序,接下来输出节点4的兄弟节点即节点5.
- 再输出它们的父节点2.
- 由于节点2的父节点是节点1,依照后序遍历的次序,应该依次寻找右子树的右孩子,一直找到不再有右孩子的节点,并输出该节点,为节点6.
- 由于节点6不存在左兄弟节点,因此输出节点6的父节点即节点3.
- 最后输出节点3的父节点1即根节点1.
至此,所有的节点都遍历输出完毕。
代码实现
采用递归的思路实现二叉树的前序、中序、后序遍历。
/**
* 构建二叉树
* param inputList 输入序列
*/
public static TreeNode createBinaryTree(LinkedList<Integer> inputList) {
TreeNode node=null;
if(inputList ==null || inputList.isEmpty()){
return null;
}
Integer data =inputList.removeFirst();
//这里的判空很关键: 如果元素是空,则不再进一步递归。
if(data !=null){
node =new TreeNode(data);
node.leftChild=createBinaryTree(inputList);
node.rightChild=createBinaryTree(inputList);
}
return node;
}
/**
* 二叉树的前序遍历
* param node 二叉树节点
*/
public static void preOrderTraveral(TreeNode node){
if(node ==null){
return;
}
System.out.println(node.data);
preOrderTraveral(node.leftChild);
preOrderTraveral(node.rightChild);
}
/**
* 二叉树的中序遍历
* param node 二叉树节点
*/
public static void inOrderTraveral(TreeNode node){
if(node==null){
return ;
}
inOrderTraveral(node.leftChild);
System.out.println(node.data);
inOrderTraveral(node.rightChild);
}
/**
* 二叉树的后序遍历
* param node 二叉树节点
*/
public static void postOrderTraveral(TreeNode node){
if(node==null){
return ;
}
postOrderTraveral(node.leftChild);
postOrderTraveral(node.rightChild);
System.out.println(node.data);
}
/**
* 二叉树节点
*/
public static class TreeNode{
int data;
TreeNode leftChild;
TreeNode rightChild;
TreeNode (int data){
this.data=data;
}
}
public static void main(String[] args){
LinkedList<Integer> inputList=new LinkedList<Integer>(Arrays.asList(new Integer[]{3,2,9,null,null,10,null,null,8,null,4}));
TreeNode treeNode=createBinaryTree(inputList);
System.out.println("前序遍历");
preOrderTraveral(treeNode);
System.out.println("中序遍历");
inOrderTraveral(treeNode);
System.out.println("后序遍历");
postOrderTraveral(treeNode);
}
二叉树用递归方式来实现前序、中序、后序遍历,是最为自然的方式,因此代码也非常简单。
这三种遍历方式的区别,仅仅是输出的执行位置不同,前序遍历的输出在前,中序遍历的输出在中间,后序遍历的输出在最后。
代码中值得注意的一点是二叉树的构建。二叉树的构建方法右很多种,这里把一个线性的链表转化成为非线性的二叉树,链表节点的顺序恰恰是二叉树的前序遍历的顺序。链表中的空值,代表二叉树节点的左孩子或右孩子为空的情况。
用golang实现二叉树的前序、中序、后序遍历。
type TreeNode struct {
Value interface{}
LeftChild *TreeNode
RightChild *TreeNode
}
//前序遍历 先根-->再左子树-->最后右子树
func preOrderTraveral(node *TreeNode) {
if node == nil {
return
}
log.Println(node.Value)
preOrderTraveral(node.LeftChild)
preOrderTraveral(node.RightChild)
}
// 中序遍历,先左子树-->再根-->最后右子树
func inOrderTraveral(node *TreeNode) {
if node == nil {
return
}
inOrderTraveral(node.LeftChild)
log.Println(node.Value)
inOrderTraveral(node.RightChild)
}
// 后序遍历,先左子树-->再右子树-->最后根
func postOrderTraveral(node *TreeNode) {
if node == nil {
return
}
postOrderTraveral(node.LeftChild)
postOrderTraveral(node.RightChild)
log.Println(node.Value)
}
func (node *TreeNode) setValue(v interface{}) {
if node ==nil {
log.Println("setting value to nil node")
return
}
node.Value=v
}
func NewNode(v interface{}) *TreeNode {
return &TreeNode{
Value: v,
}
}
func main() {
// 构造二叉树
root := NewNode(3)
root.LeftChild=&TreeNode{}
root.LeftChild.setValue(2)
root.LeftChild.LeftChild=NewNode(9)
root.LeftChild.RightChild=NewNode(10)
root.RightChild=NewNode(8)
root.RightChild.RightChild=NewNode(4)
log.Println("前序遍历")
preOrderTraveral(root)
log.Println("中序遍历")
inOrderTraveral(root)
log.Println("后序遍历")
postOrderTraveral(root)
}