树的深度遍历
深度优先遍历有前中后三种情况,前面已经介绍过深度遍历的具体概念,本篇介绍将介绍使用递归和迭代去实现树的深度优先遍历
1、前序遍历
1.1、递归实现前序遍历
递归,就是自己调用自己,就像下面这个样子,前面的每一层都去一模一样地去调用下一层,不同的只是输入和输出的参数,这个递推过程是写递归的第一个核心问题。
当然这个过程不能一直持续下去,一定要在满足某个要求之后返回结果的,否则的话,就会抛出"StackOverFlow"问题。
所有的递归都有两个基本的特征:
(1)执行时范围不断缩小,这样才能触底反弹。
(2)终止判断在递归调用之前。
递归的可以按照如下三个步骤来写:
1.从小到大递推
2.分情况讨论,明确结束条件
3.组合出完整方法
下面以树的前序遍历来举例说明
第一步:从小到大递推
二叉树:[3, 9, 20, null, null, 15, 7]
3
/ \
9 20
/ \
15 7
其前序遍历结果:[3, 9, 20, 15, 7]
先选一个最小的子树,即20为根节点的树,假如20为根节点,则此树的前序遍历访问顺序应该是:
void visit1(){
root; //20被访问
root.left; //继续访问15
root.right; //继续访问7
}
然后再向上访问,看根节点为3的情况:
void visit2(){
root; //3被访问
root.left; //继续访问9
root.right; //继续访问20
}
这里20是一个子树的父节点,访问方式与上面的visit1一样,所以我们可以直接合并到一起:
void visit(){
root;
visit(root.left);
visit(root.right);
}
第二步:分情况讨论,明确结束条件
上面有了递归的主体,但是这个递归什么时候结束呢?很明显,在这里应该是root == null的时候。一般来说链表和二叉树的问题终止条件都包含当前访问的元素为null。
第三步:组合出完整方法
找到了递归主体和终止条件之后,就能将完整的代码写出来了,代码如下:
public void perorder(TreeNode root, List<Integer> res) {
if (root == null) {
return;
}
res.add(root.val);
preorder(root.left, res);
preorder(root.right, res);
}
在写出了完整的递归函数之后,可以画一个调用过程图推演一下递归函数是不是正确的,下图就是针对刚刚前序遍历代码的递归过程图:
从图中可以看出,当root的一个子树为null的时候还是会执行递归,进入之后发现root == null了,然后就开始返回。
1.2、迭代实现前序遍历
前序遍历除了使用递归法,还能用迭代去做。递归其实就是每次执行方法调用时都会先把当前的局部变量、参数值和返回地址等压入栈中,后面在递归返回的时候,从栈顶弹出上一层的各项参数继续执行,这就是递归可以自动返回并执行上一层方法的原因。而迭代法就是显示地把栈表示出来。所以理论上递归能做的事情,迭代都能做,只不过有些处理起来会比较复杂。
前序遍历是中左右,如果还有左子树就一直往下找,完了之后再返回从底层逐步向上向右找。基于此可以写出如下代码实现(注意代码中,空节点不入栈):
public List<Integer> preOrderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
if (root == null) {
return res;
}
Deque<TreeNode> stack = new LinkedList<TreeNode>();
TreeNode node = root;
while(!stack.isEmpty() || node != null) {
while(node != null) {
res.add(node.val);
stack.push(node);
node = node.left;
}
node = stack.pop();
node = node.right;
}
return res;
}
2、中序遍历
2.1、递归实现中序遍历
中序遍历与前序遍历的不同点在于中序遍历中的父节点是在左子节点和右子节点中间访问的,所以只需要改变前序遍历递归代码中输出父节点的位置的好了,代码如下:
public void inOrder(TreeNode root, List<Integer> res) {
if (root == null) {
return;
}
inOrder(root.left, res);
res.add(root.val);
inOrder(root.right, res);
}
2.2、迭代实现中序遍历
中序遍历是左中右,先访问的是二叉树左子树的节点,然后一层一层向下访问,直到到达树左边的最底部,再开始处理节点(也就是再把节点的数值放进res列表中)。再使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用力啊处理节点上的元素。代码如下:
public List<Integer> inOrderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
Deque<TreeNode> stack = new LinkedList<TreeNode>();
while(!stack.isEmpty() || root != null) {
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
res.add(root.val);
root = root.right;
}
}
3、后序遍历
3.1、递归实现后序遍历
后序遍历是左右中,在遍历中将父节点最后访问即可,代码如下:
public void postOrder(TreeNode root, List<Integer> res) {
if (root == null) {
return;
}
postOrder(root.left, res);
postOrder(root.right, res);
res.add(root.val);
}
3.2、迭代法实现后序遍历
直接用迭代去实现后序遍历比较复杂,这里可以用一种比较巧妙的方法去实现——反转法。
方法解释如下:
二叉树:[3, 9, 20, null, null, 15, 7]
3
/ \
9 20
/ \
15 7
后序遍历结果为[9, 15, 7, 20, 3]
如果将结果整体反转过来的话,就是[3, 20, 7, 15, 9]
可以发现,得到反转后序列的方法和迭代得到前序遍历的思路基本是一致的,只不过左右反过来了。前序是先中间,再左边然后右边,而这里是先中间,再右边然后左边。可以直接改造一下前序遍历中的代码,得到反转后的序列,再把序列反转回后序遍历的模样就可以了。
代码实现如下:
public List<Integer> preOrderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
if (root == null) {
return res;
}
Deque<TreeNode> stack = new LinkedList<TreeNode>();
TreeNode node = root;
while(!stack.isEmpty() || node != null) {
while(node != null) {
res.add(node.val);
stack.push(node);
node = node.right;
}
node = stack.pop();
node = node.left;
}
Collections.reverse(res); //反转序列
return res;
}
4、总结
本文主要讲述了树的深度优先遍历的代码实现,用递归去解决树的深度优先遍历非常简单,几行代码就足够了,对于前序中序和后序只需要把中间的父节点的输出位置改一下就好了。而使用迭代法去解决深度遍历的问题,比递归会要麻烦一点,主要是要处理好节点处理的次序,要跟遍历的次序一致,前序遍历就是先处理中间的,如果还有左子节点就一直往下找,直到找完了,再逐层向上向右找。中序遍历就是一直往下找,找到树最左边的位置,再开始处理节点。后序遍历直接处理比较麻烦,可以反转过后借助前序遍历的思路去处理,处理完了再反转回去。
总之,对于树的深度遍历,主要要考虑的就是节点的访问次序,处理好这个之后,其他地方就不麻烦了。