目录
一,构建二叉树
二叉树是在计算机中非常常用的一种数据结构。二叉树中每个节点最多只能有两个子节点。
很多时候我们要对整个树进行遍历,遍历操作不仅仅是对二叉树有意义,其他所有的树都可能要进行遍历操作,另外图型数据结构中遍历也是一个非常常规的操作。在这里可以通过二叉树的遍历来体会一下遍历这种操作。
一颗二叉树的基本代码如下。
/**
* 二叉树
*
*/
public class BST<k, v> {
/**
* 树中的节点为私有的类, 外界不需要了解二叉树节点的具体实现
*
*/
private class Node{
private k key;
private v value;
private Node left, right;
public Node(k key, v value){
this.key = key;
this.value = value;
this.left = this.right = null;
}
}
private Node root; // 根节点
private int count; // 树中的节点个数
/**
* 构造函数, 默认构造一棵空二叉树
*/
public BST(){
root = null;
count = 0;
}
/**
* 返回二叉树的节点个数
* @return
*/
public int size(){
return count;
}
/**
* 返回二叉树是否为空
* @return
*/
public boolean isEmpty(){
return count == 0;
}
}
二、二叉树的遍历-深度优先遍历
二叉树分前中后序遍历:
前序遍历:先访问当前节点,再依次递归访问左右子树。
中序遍历:先递归访问左子树。再访问自身,再递归访问右子树。
后续遍历:先递归访问左右子树,再访问自身节点。
我们先来理解一下三种遍历顺序,对于每个节点来说,它都有左右两颗子树,
对于后续遍历还说,它有一个特点,就是已经将当前节点的左右两个子树都遍历完成之后,才开始做它要做的事情。这样的一个性质在进行某些操作的时候,是非常方便的。最典型的一个应用就是当我们释放整个二叉树的时候,我们需要将每一个节点的左右两个子树上的节点都释放完成之后,才应该释放自身,所以这个过程就应该使用后续遍历。后面会用代码来具体的讲解这个释放的操作。
下面来看一下具体的代码实现。
/**
* 二叉树的前序遍历
*/
public void preOrder(){
preOrder(root);
}
/**
* 二叉树的中序遍历
*/
public void inOrder(){
inOrder(root);
}
/**
* 二叉树的后序遍历
*/
public void postOrder(){
postOrder(root);
}
/**
* 对以node为根的二叉树进行前序遍历, 递归算法
* @param node
*/
private void preOrder(Node node){
if(node != null){
System.out.println(node.key);
preOrder(node.left);
preOrder(node.right);
}
}
/**
* 对以node为根的二叉树进行中序遍历, 递归算法
* @param node
*/
private void inOrder(Node node){
if(node != null){
inOrder(node.left);
System.out.println(node.key);
inOrder(node.right);
}
}
/**
* 对以node为根的二叉树进行后序遍历, 递归算法
* @param node
*/
private void postOrder(Node node){
if(node != null){
postOrder(node.left);
postOrder(node.right);
System.out.println(node.key);
}
}
每种遍历的顺序都有各自的特点,可以在不同的使用场景中发挥不同的作用。上面代码实现的遍历只是把key打印出来而已,当然你可以做任何其他操作。
三、层序遍历-广度优先遍历
这里我们介绍一下二叉树的层序遍历,于此同时介绍一个在层序遍历背后更加重要的概念 广度优先遍历,
我们上面实现的二叉树的遍历,不管是前序、中序和后序遍历都是深度优先遍历,遍历的时候首先尝试着移动到最深层的节点。如下图所示的例子中首先尝试着会走到到元素13,走不通了之后,才会返回用回溯的方式,这样将整个树遍历完成。
那么和深度优先遍历相对应的,就是广度优先遍历。广度优先遍历对应到二叉树上,就是层序遍历。也就是说如下例子,当我们遍历完节点28之后,接下来遍历的是下一层的节点16和30,记下来继续遍历下一层的所有节点,依次类推一层一层的往下遍历。我们可以看到对于这种方式来说,我们没有优先从根节点一直走到最深的某一个叶子节点,而是更加关注广度,将每一层的所有节点优先遍历完毕,所以这就叫做广度优先遍历。
具体的广度优先遍历应该怎么实现呢?通常实现一个广度优先遍历,需要引入一个队列。所谓的队列就是一种先进先出,后进后出的数据结构。
下面我们来分析一下广度优先遍历的具体实现。
如下图所示,首先获取到二叉树的根节点,把根节点入队。
开启一个循环,在每次循环中,只要队列不为空,就将队首元素出队,然后该元素的左右子节点入队。比如说现在队列不为空只有一个元素。那么我们就把该元素出队。将该元素取出来就是遍历到了该元素,就可以对该元素做相应的操作,最简单的像打印出该元素。
之后将出队的节点的左右两个子节点入队,对于节点28来说它的左右两个子节点分别是16和30,所有要将16和30两个节点入队。
接下来继续循环,将队首的元素16出队,做相应的操作。将节点16的左右两个子节点13和22入队。
接下来将队首的元素30出队,做相应的操作。将节点30的左右两个子节点129和42入队。
接下来继续将队首元素13出队,然后将元素13的左右两个子节点入队,但是节点13没有子节点,所以就没有入队操作。
之后我们继续讲队首元素22、29、42依次出队,由于它们都没有子节点,所以都不需要入队。
此时我们的队列已经为空了,整个遍历操作完成了。
我们可以看看图中右侧的打印输出,就是这个按二叉树的层序从上到下从左到右打印出来的。
以上过程就是二叉树的广度优先遍历的过程。
下面是具体的代码实现。
/**
* 二叉树的层序遍历 广度优先搜索
*/
public void levelOrder(){
//使用LinkedList来作为我们的队列
Queue<Node> q = new LinkedList<Node>();
q.add(root);
if(!q.isEmpty()){
Node node = q.remove();
System.out.println(node.key);
if(node.left != null){
q.add(node.left);
}else if(node.right != null){
q.add(node.right);
}
}
}
四、总结
上面我们将二叉树的前中后序遍历和层序遍历,四种遍历方式全部介绍完了。最后值得一提的是这四种遍历方式都是比较高效的,时间复杂度O(n)。遍历一次至少每个节点需要遍历到,我们可以回忆一下四种遍历方式,每一种遍历方式每一个节点只访问了常数次,真是因为如此使的整个时间复杂度是O(n)。
这些遍历方法是非常重要的,很多时候可能不需要显示的构建出来一棵树,但也需要运用类似的遍历方式。我们可以思考一下归并排序和快速排序他们本质其实是一颗二叉树的深度优先遍历的过程。那么与此同时这四种遍历,也非常好的结合了递归这种计算机中非常常见的技术,以及使用队列这样的数据结构来实现一个更加复杂的算法。这样一个过程,使用基础的数据结构,构建更加复杂的算法的过程。