前言
递归法实现二叉树的前、中、后序遍历的代码比较容易理解,不过迭代法来实现就比递归法稍微要复杂一点,所以我就将迭代法和递归法实现二叉树的前中后序遍历来做一个总结,将三种遍历的迭代法和递归法放到一起来,方便大家的理解,以及发现其中的共同点
一、如何通过迭代来实现递归?
通过观察递归的规律,我们不难发现,递归调用的方法有一个特点,那就是先调用的方法,后执行,后调用的方法,先执行。这不就是先进后出,后进先出的特点吗?符合这个特点的那就是数据结构中的:栈;
其实递归就是隐式的帮我们维护了一个栈,所以要通过迭代来实现递归,我们就可以显式的维护一个栈,来实现递归的过程。
二、类的声明
二叉树的每个节点其实都是某个类的实例,此处把该类先写出来,方便后续代码的理解。至于二叉树的构建,不是此文章关注的重点
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
三、迭代法
下面代码的方法形参 TreeNode root 传入的是一棵已经构建好的二叉树的根节点
1.前序遍历
前序遍历的特点就是:根节点 → 左结点 → 右节点
大致思路就是:每次从栈中取出一个节点,然后将该节点的值存入结果集中,如果该节点左右子结点都不为空,那么就先将右结点压入栈,然后将左结点压入栈。这样子下次从栈中取出的结点就是左结点。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
//返回的结果集
List<Integer> res = new ArrayList<>();
//如果根节点为空,那么就直接返回空的结果集,没遍历的必要了
if(root == null) return res;
//创建一个栈
Deque<TreeNode> stack = new LinkedList<>();
//将当前的根节点放入栈中
stack.push(root);
//如果当前的栈不为空,那就一直循环
while(!stack.isEmpty()){
//从栈中取出一个结点
root = stack.pop();
//由于是前序遍历,所以将当前的结点直接存入结果集合中
res.add(root.val);
//此处的顺序比较有将就,前序是:根 → 左 → 右
//所以我们要先将右结点压入栈,再将左结点压入栈
//因为左结点是最晚压入栈的,所以取的时候,会先取左结点,然后是右结点,就符合前序遍历了
if(root.right != null) stack.push(root.right);//如果当前结点的右子结点不为空,就把右子结点压入栈
if(root.left != null) stack.push(root.left);//如果当前节点的左子结点不为空,就把左子结点压入栈
}
return res;
}
}
2.后序遍历
为什么我不把后序遍历放到最后讲呢,因为后序遍历和前序遍历有着异曲同工之处。
前序遍历是:根 → 左 → 右
后序遍历是:左 → 右 → 根
把前序遍历的左、右结点遍历的顺序给修改一下为:根 → 右 → 左,然后这个顺序从右往左读,那不就是后序遍历的顺序吗?所以后序遍历的代码,我们只需要在前序遍历的基础上,修改一点点就是后序遍历了。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
//此处的结果集就不能再声明为List了,我们需要使用到双端队列的功能,所以声明为LinkedList
LinkedList<Integer> res = new LinkedList<>();
//如果根节点为空,那么就直接返回空的结果集,没遍历的必要了
if(root == null) return res;
//创建一个栈
Deque<TreeNode> stack = new LinkedList<>();
//将当前的根节点放入栈中
stack.push(root);
//如果当前的栈不为空,那就一直循环
while(!stack.isEmpty()){
//从栈中取出一个结点
root = stack.pop();
//此处跟前序遍历不一样,前序遍历是往结果集的末尾添加,而后续遍历是前序遍历变形后的倒序输出
//所以每次取出的结点都从结果集头部插入,这样子就能实现将前序遍历的变形倒序输出了
res.addFirst(root.val);
//因为后序是:左 → 右 → 根,上一行代码我们已经插入了根节点,所以接下来该再插入右结点,然后再插入左结点(注意,每次插入都是从结果集头部插入)
//所以右结点应该被先取出,左结点最后被取出
//根据栈的特性,最后被取出那就得先入栈,所以先将左结点入栈,然后是右结点入栈
if(root.left != null) stack.push(root.left);//如果当前结点的右子结点不为空,就把右子结点压入栈
if(root.right != null) stack.push(root.right);//如果当前节点的左子结点不为空,就把左子结点压入栈
}
return res;
}
}
所以可以发现,前序遍历的迭代法和后序遍历的迭代法,两者只有小小的区别,了解其中一种,就不难写出另外一种。
3.中序遍历
中序遍历就是:左 → 根 → 右
大致思路:从根节点开始,不断的将该节点的左结点入栈,直到左结点为空,然后从栈中取出该节点,将结点值存入结果集中,然后再指向当前节点的右结点。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
//返回的结果集
List<Integer> res = new ArrayList<>();
//创建一个栈
Deque<TreeNode> stack = new LinkedList<>();
//如果当前节点不为空或者当前栈不为空,就继续循环
while(root != null || !stack.isEmpty()){
//如果当前节点不为空,就继续循环
//这个循环的目的是找到当前结点最左边的子结点
while(root != null){
//将当前结点压入栈
stack.push(root);
//调整root指向,指向左节点
root = root.left;
}
//从栈中取出结点
root = stack.pop();
//将该值添加进结果集
res.add(root.val);
//调整root指向,指向右节点
root = root.right;
}
return res;
}
}
四、递归法
递归法的代码就比较直观,书写的顺序也跟遍历的顺序一样
1.前序遍历
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> preorderTraversal(TreeNode root) {
preOrder(root);
return res;
}
public void preOrder(TreeNode root){
if(root == null) return;
res.add(root.val);//当前节点存入结果集
preOrder(root.left);//递归左子结点
preOrder(root.right);//递归右子结点
}
}
2.中序遍历
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
infixOrder(root);
return res;
}
public void infixOrder(TreeNode root){
if(root == null) return;
infixOrder(root.left);//递归左子结点
res.add(root.val);//当前节点存入结果集
infixOrder(root.right);//递归右子结点
}
}
3.后序遍历
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
postOrder(root);
return res;
}
public void postOrder(TreeNode root){
if(root == null) return;
postOrder(root.left);//递归左子结点
postOrder(root.right);//递归右子结点
res.add(root.val);//当前节点存入结果集
}
}
总结
二叉树的前中后序遍历是挺重要的一个知识点,所以将二叉树的遍历方法在此做一个总结,记不得了随时可以回顾,有不懂的欢迎评论留言~