漫画算法-学习笔记(08)

漫画算法-小灰的算法之旅(08)

1. 二叉树的遍历

1.1 为什么要研究遍历

二叉树的遍历有什么特殊之处?

在计算机程序中,遍历本身是一个线性操作。所以遍历同样具有线性结构数组、链表是一件很容易的事情。反观二叉树,是典型的非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列,以不同的方式来遍历,遍历出的序列顺序也不同。

二叉树的遍历方式有哪些?

从节点之间的位置关系的角度来看,二叉树的遍历分为4种。

  • 前序遍历
  • 中序遍历
  • 后序遍历
  • 层序遍历

从更宏观的角度来看 ,二叉树的遍历归结为两大类。

  • 深度优先遍历(前序遍历、中序遍历、后序遍历)
  • 广度优先遍历(层序遍历)
1.2 深度优先遍历

深度优先和广度优先这两个概念不止局限与二叉树,它们更是一种抽象的算法思想,决定了访问某些复杂数据结构的顺序。在访问等其他一些复杂数据结构时,这两个概念常常被使用到。

所谓深度优先:就是偏向于纵深,“一头扎到底”的访问方式。可能这种说法有些抽象。下面通过二叉树的前序遍历、中序遍历、后序遍历来理解深度优先到底是怎么回事。

前序遍历(根左右)

二叉树的前序遍历,输出顺序是根节点、左子树,右子树

上图就是一个二叉树的前序遍历,每个节点左侧的序号代表该节点的输出顺序,详细步骤如下。

  1. 首先输出的是根节点1.
  2. 由于根节点1存在左孩子,因此输出左孩子节点2.
  3. 由于节点2也存在左孩子,输出左孩子节点4.
  4. 节点4既没有左孩子,也没有右孩子,那么回到节点2,输出节点2点右孩子节点5.
  5. 节点5既没有左孩子,也没有右孩子,那么回到节点1,输出节点1点右孩子节点3.
  6. 节点3没有左孩子,但右右孩子,因此输出节点3点右孩子节点6.

至此,所有的节点都遍历输出完毕。

中序遍历(左根右)

二叉树的中序遍历,输出顺序是左子树,根节点,右子树。

上图就是一个二叉树的中序遍历,每个节点左侧的序号代表该节点的输出顺序,详细步骤如下。

  1. 首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子的节点,并输出该节点。显然,第一个没有左孩子的节点是节点4.
  2. 依照中序遍历的次序,接下来输出节点4的父节点2.
  3. 再输出节点2的有孩子节点5
  4. 以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的根节点1.
  5. 由于节点3没有左孩子,所以直接输出根节点1的右孩子节点3.
  6. 最后输出节点3的右孩子节点6.

至此,所有的节点都遍历输出完毕。

后序遍历(左右根)

二叉树的后序遍历,输出顺序是左子树,右子树,根节点。

上图就是一个二叉树的后序遍历,每一个节点左侧的序号代表该节点的输出顺序。详细步骤如下。

  1. 首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子的节点,并输出该节点。显然第一个没有左孩子的节点是节点4.
  2. 依照后序遍历的次序,接下来输出节点4的兄弟节点即节点5.
  3. 再输出它们的父节点2.
  4. 由于节点2的父节点是节点1,依照后序遍历的次序,应该依次寻找右子树的右孩子,一直找到不再有右孩子的节点,并输出该节点,为节点6.
  5. 由于节点6不存在左兄弟节点,因此输出节点6的父节点即节点3.
  6. 最后输出节点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)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值