二叉树的遍历
一,二叉树的遍历定义
二叉树的遍历是指按指定的规律对二叉树中的每个结点访问一次且仅访问一次.
所谓访问是指对结点做某种处理,二叉树的基本组成:根结点、左子树、右子树。若能依次遍历这三部分,就是遍历了二叉树。
1,二叉树的先中后序遍历
若以L、D、R分别表示遍历左子树、遍历根结点和遍历右子树,则有六种遍历方案:DLR、LDR、LRD、DRL、RDL、RLD。若规定先左后右,则只有前三种情况三种情况,分别是:
DLR——先(根)序遍历。
LDR——中(根)序遍历。
LRD——后(根)序遍历。
递归遍历算法具有非常清晰的结构。实际上,递归算法是由系统通过使用堆栈来实现控制的。而非递归算法中的控制是由设计者定义和使用堆栈来实现的。
2,二叉树的层次遍历
层次遍历二叉树,是从根结点开始遍历,按层次次序“自上而下,从左至右”访问树中的各结点。为保证是按层次遍历,必须设置一个队列用来辅助.
下面我们就尝试实现上面的二叉树的遍历算法
二,二叉树遍历算法
下面算法讲解都是基于下面这个二叉树进行分析
二叉树的节点定义如下:
/**
* 二叉树节点
* @param <T> 节点值
*/
public class TreeNode<T> {
TreeNode<T> left;
TreeNode<T> right;
T value;
public TreeNode( T value) {
super();
this.value = value;
}
}
1,先序遍历
(1)先序递归遍历
先序遍历是如果一棵二叉树根节点非空,那么先访问他的根节点,然后再递归访问他的左子树,最后递归访问他的右子树.
对于上面的二叉树,先序遍历为: abdgcef
首先访问根节点a,然后访问他的左子树b,以b为根节点的左子树(为空),以b为根节点的右子树d,以d为根节点的左子树g,以b为根节点的右子树(为空),访问以g为根节点的左子树(为空),以g为根节点的右子树(为空)
a的左子树遍历完成后,接下来访问a的右子树c,然后是c的左子树e,c的右子树f,访问以e为根节点的左子树(为空),访问以e为根节点的右子树(为空),访问以f为根节点的左子树(为空),访问以f为根节点的右子树(为空)
由此我们可以推出二叉树的先序遍历算法过程如下:
如果根节点root为空,则遍历完成
否则
递归先序遍历根节点root的左子树
递归先序遍历根节点root的右子树
对应的伪代码如下:
如果root为空则返回
否则
遍历根节点
递归遍历左子树 preOrderByRecurse(root.left)
递归遍历右子树 preOrderByRecurse(root.right)
实现代码为:
public void preOrderByRecurse(TreeNode<T> root) {
if (root == null) {
return;
}
System.out.print(root.value);
preOrderByRecurse(root.left);
preOrderByRecurse(root.right);
}
(2)先序非递归遍历
先序非递归遍历需要使用到栈这个数据结构,遍历过程如下:
如果根节点为空,则返回
否则,根节点入栈
while(栈中元素个数大于0){
A,出栈栈顶元素node,访问根节点node
B,如果根节点node的右子树node.right不为空,进栈
C,如果根节点node的左子树node.left不为空,进栈
}
根据上面的图形,我们可以模拟栈中的情形
首先a进栈 栈中元素:a
a出栈, a.right c进栈,a.left b进栈. 栈中元素:c,b
b出栈, b.right d进栈,b.left为空,跳过 栈中元素:c,d
d出栈,d.right为空,跳过,d.left g进栈 栈中元素:c,g
g出栈, g.right为空,跳过,g.left为空,跳过栈中元素:c
c出栈, c.right f进栈,c.left e进栈栈中元素:f,e
e出栈, e.right为空,跳过,e.left为空,跳过栈中元素:f
f出栈, f,right为空,跳过,f,left为空,跳过 栈为空
栈为空,遍历结束
对应的代码如下:
public void preOrder(TreeNode<T> root) {
if (root == null) {
return;
}
List<TreeNode<T>> stack = new ArrayList<TreeNode<T>>();
stack.add(root);
TreeNode<T> node = null;
while (stack.size() > 0) {
// 遍历根节点
node = stack.remove(stack.size() - 1);
System.out.print(node.value);
// 右子树进栈
if (node.right != null) {
stack.add(node.right);
}
// 左子树进栈
if (node.left != null) {
stack.add(node.left);
}
}
}
2,中序遍历
(1)中序递归遍历
如果一个二叉树不为空,那么
先递归访问他的左子树,然后访问根节点,最后递归访问他的右子树
对于上面的二叉树,其中序遍历为:bgdaecf
对应的伪代码是:
如果根节点为空,则返回
否则
递归遍历左子树 inOrderRecurse(root.left)
访问根节点
递归遍历右子树 inOrderRecurse(root.right)
对应的代码是:
public void inOrderRecurse(TreeNode<T> root) {
if (root == null) {
return;
}
inOrderRecurse(root.left);
System.out.print(root.value);
inOrderRecurse(root.right);
}
(2)中序非递归遍历
中序非递归遍历同样也要用到栈这个数据结构.遍历过程如下:
如果根节点为否,返回
否则
将根节点赋值node变量
while(node != null || 栈中元素个数大于0){
while(node != null){
node入栈
node赋值node的左子树根节点
}
//上面把其左子树一直入栈,直至有一个为空
移除栈顶元素,并访问且赋值给node变量
node = node.right
}
我们在使用上面的二叉树图来模拟这个算法
首先,a赋予node 然后进入循环进行判断
a入栈,然后a.left b入栈,接着由于b.left为空,所以跳出内层循环 栈中元素a,b
b出栈,d入栈,g入栈,由于g.left为空,所以g出栈栈中元素a,d
由于g.right也为空,所以接下来 d出栈栈中元素a
d.right也为空,所以a出栈 栈为空
a.right c入栈, c.left e入栈,e.left为空,出栈栈中元素 c
由于e.right也为空,所以c出栈 栈为空
接下来c.right入栈 栈中元素c
由于c.right和c.left都为空, c出栈栈为空
结束遍历
对应的代码如下:
public void inOrder(TreeNode<T> root) {
if (root == null) {
return;
}
List<TreeNode<T>> stack = new ArrayList<TreeNode<T>>();
TreeNode<T> node = root;
while (node != null || stack.size() > 0) {
while (node != null) {
stack.add(node);
node = node.left;
}
node = stack.remove(stack.size() - 1);
System.out.print(node.value);
node = node.right;
}
}
3,后序遍历
(1)后序递归遍历
如果二叉树为空,则返回
否则,递归遍历其左子树,递归遍历其右子树,访问根节点
对于上图,后序遍历结果是: gdbefca
对应的代码是:
public void postOrderRecurse(TreeNode<T> root) {
if (root == null) {
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print(root.value);
}
(2)后序非递归遍历
后序非递归遍历与前两种有一定的区别,那就是当一个具有右子树节点遍历完其右子树的时候,怎么判断是遍历该节点还是以该节点右子树为根节点的子树,这里我们使用一个flag标志位和TreeNode变量用来标志,同样我们还需要使用一个辅助数据结构栈
对应伪代码是:
TreeNode node = root;
do{
//左子树循环入栈
while(node != null){
node入栈
node=node.left
}
//通过flag和temp来进行判断是访问右节点还是以右节点为根节点的子树
boolean flag = true;
TreeNode temp = null;
while(flag && 栈中元素个数大于0){
取得栈顶元素赋值node
if(node.right == temp){
//说明要访问该节点
栈顶元素出栈访问且赋值temp
}else{
//遍历其右子树
flag = false;
node = node.right;
}
}
}while(栈中元素个数大于0)
对应的代码如下:
public void postOrder(TreeNode<T> root) {
if (root == null) {
return;
}
List<TreeNode<T>> stack = new ArrayList<TreeNode<T>>();
TreeNode<T> node = root;
do {
// 左子树进栈
while (node != null) {
stack.add(node);
node = node.left;
}
// 判断右子树是否已经访问过和右子树进栈 ,通过 flag标志来表示右子树是否需要进栈.通过node.right ==
// temp来判断右子树是否已经访问过
boolean flag = true;
TreeNode<T> temp = null;
while (flag && stack.size() > 0) {
node = stack.get(stack.size() - 1);
if (node.right == temp) {
node = stack.remove(stack.size() - 1);
System.out.print(node.value);
temp = node;
} else {
node = node.right;
flag = false;
}
}
} while (stack.size() > 0);
}
4,层次遍历
(1)层次遍历
层次遍历比较简单,按照二叉树的层次一层一层的由上及下,由左及右的进行访问.
层次遍历我们要使用到辅助数据结构队列
对应的伪代码
根节点入队列
while(队列不为空){
出队元素赋值node节点
如果node.left不为空,node.left入栈
如果node.right不为空,node.right入栈
}
对应的层次遍历结果是:
abcdefg
对应的代码是:
public void traverseByLevel(TreeNode<T> root) {
if (root == null) {
return;
}
LinkedList<TreeNode<T>> queue = new LinkedList<TreeNode<T>>();
int slow = 0, fast = 1;
int level = 0;
queue.add(root);
while (queue.size() > 0) {
TreeNode<T> node = queue.removeFirst();
System.out.print(node.value);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
(2)带层次数字的层次遍历
遍历结果如下:
1:a
2:bc
3:def
4:g
我们可以在层次遍历的基础上进行改进,我们所有的节点都是存放在了队列中,当我们打印行号的时候,关键是我们怎么知道每一层在那个节点是每一层的最后一个节点.
这里我们可以使用三个索引, front指向要队列头元素, rear指向队列为元素, last指向每一层结束的最后一个元素
对应的代码是:
public void traverseByLevelNumber(TreeNode<T> root) {
if (root == null) {
return;
}
LinkedList<TreeNode<T>> queue = new LinkedList<TreeNode<T>>();
int front = 0, rear = 1, last = 1;
int level = 0;
queue.add(root);
TreeNode<T> node = null;
System.out.print(level + ": ");
while (front != rear) {
node = queue.get(front);
if (node != null && node.left != null && front != last) {
queue.add(node.left);
rear++;
}
if (node != null && node.right != null && front != last) {
queue.add(node.right);
rear++;
}
front++;
System.out.print(node.value);
if (front == last) {
last = rear;
level++;
System.out.println();
System.out.print(level + ": ");
}
}
}