巧解二叉树遍历问题(方法 + 代码 + 解析)

157 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情 >>

巧解二叉树遍历问题

相信学过数据结构的各位朋友们对于二叉树还是非常熟悉的,对二叉树的遍历更是一个离不开的话题。

那么今天咱们就一起来聊一聊二叉树的各种遍历方法。

二叉树遍历

在介绍二叉树的遍历方法之前,我们先来看看二叉树有多少种遍历。

  1. 前序遍历:先根节点,在左节点、右节点
  2. 中序遍历:先左节点,再根节点,最后右节点
  3. 后序遍历:先左节点,再右节点,最后根节点

一、递归遍历法

利用函数递归的方法对二叉树进行顺序遍历。这种方法需要对函数递归方面比较熟悉才能够清晰地体会到它的巧妙之处。

下面我们一起来看看这几种遍历方法的具体实现,如下:

利用递归的方法对二叉树实现前序遍历:

 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        // 调用递归函数
        preVisit(root, list);
        return list;
    }
    // 1、确定递归函数的参数和返回值类型
    public void preVisit (TreeNode root, List<Integer> list) {
        // 2、确定递归终止条件
        if (root == null) {
            return;
        }
        // 访问节点
        list.add(root.val);
        
        // 以下为确定单层遍历的代码
        if (root.left != null) {
            preVisit(root.left,list);
        }
​
        if (root.right != null) {
            preVisit(root.right, list);
        }
        return;
    }

利用递归的方法对二叉树实现中序遍历:

 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        // 调用递归函数
        preVisit(root, list);
        return list;
    }
    // 1、确定递归函数的参数和返回值类型
    public void preVisit (TreeNode root, List<Integer> list) {
        // 2、确定递归终止条件
        if (root == null) {
            return;
        }
        
        // 以下为确定单层遍历的代码
        if (root.left != null) {
            preVisit(root.left,list);
        }
​
        // 访问节点
        list.add(root.val);
​
        if (root.right != null) {
            preVisit(root.right, list);
        }
        return;
    }

利用递归的方法对二叉树实现后序遍历:

 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        // 调用递归函数
        preVisit(root, list);
        return list;
    }
    // 1、确定递归函数的参数和返回值类型
    public void preVisit (TreeNode root, List<Integer> list) {
        // 2、确定递归终止条件
        if (root == null) {
            return;
        }
        
        // 以下为确定单层遍历的代码
        if (root.left != null) {
            preVisit(root.left,list);
        }
​
        if (root.right != null) {
            preVisit(root.right, list);
        }
        
        // 访问节点
        list.add(root.val);
        
        return;
    }

上述三种遍历,采取的都是同一种方法思想,而且代码也是大同小异,所以这种方法对于我们进行二叉树遍历是非常便利的。

二叉树三种遍历就是访问节点的顺序不同,所以在这个递归方法中可以很明显地感受出来,

访问节点的代码分别就出现在单层代码的前、中、后位置

二、迭代法遍历二叉树

前序遍历:

 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null){
            return list;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            list.add(node.val);
            if (node.right != null){
                stack.push(node.right);
            }
            if (node.left != null){
                stack.push(node.left);
            }
        }
        return list;
    }

中序遍历:

 public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null){
            return list;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()){
           if (cur != null){
               stack.push(cur);
               cur = cur.left;
           }else{
               cur = stack.pop();
               list.add(cur.val);
               cur = cur.right;
           }
        }
        return list;
    }

后序遍历:

 public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null){
            return list;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            list.add(node.val);
            if (node.left != null){
                stack.push(node.left);
            }
            if (node.right != null){
                stack.push(node.right);
            }
        }
        Collections.reverse(list);
        return list;
    }

通过上述代码可以看到这一次三种遍历的代码就有了很大的变化了,三种遍历的代码就不具有统一的风格了。

这里面最主要的原因是,在自己实现进出栈的时候,会随着遍历次序不同而导致进出栈的条件不同

三、统一的迭代遍历法

前序遍历:

 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.empty()) {
            TreeNode node = stack.peek();
            if (node != null) {
                // 结点先进栈再出栈
                // 进栈是为了不断推进下一个根节点
                // 出栈是为了重新调整根节点、左右节点的栈中顺序
                stack.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node.right!=null) stack.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) stack.push(node.left);    // 添加左节点(空节点不入栈)
                stack.push(node);                          // 添加中节点
                stack.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                stack.pop();           // 将空节点弹出
                node = stack.peek();    // 重新取出栈中元素
                stack.pop();
                list.add(node.val); // 加入到结果集
            }
        }
        return list;
    }

中序遍历:

public List<Integer> inorderTraversal(TreeNode root) {
     List<Integer> list = new LinkedList<>();
    Stack<TreeNode> stack = new Stack<>();
    if (root != null) stack.push(root);
    while (!stack.empty()) {
        TreeNode node = stack.peek();
        if (node != null) {
            // 结点先进栈再出栈
           // 进栈是为了不断推进下一个根节点
           // 出栈是为了重新调整根节点、左右节点的栈中顺序
            stack.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
            if (node.right!=null) stack.push(node.right);  // 添加右节点(空节点不入栈)
            stack.push(node);                          // 添加中节点
            stack.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
​
            if (node.left!=null) stack.push(node.left);    // 添加左节点(空节点不入栈)
        } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
            stack.pop();           // 将空节点弹出
            node = stack.peek();    // 重新取出栈中元素
            stack.pop();
            list.add(node.val); // 加入到结果集
        }
    }
    return list;
}

后序遍历:

  public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.empty()) {
            TreeNode node = stack.peek();
            if (node != null) {
                // 结点先进栈再出栈
                // 进栈是为了不断推进下一个根节点
                // 出栈是为了重新调整根节点、左右节点的栈中顺序
                stack.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                stack.push(node);                          // 添加中节点
                stack.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                if (node.right!=null) stack.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) stack.push(node.left);    // 添加左节点(空节点不入栈)         
                               
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                stack.pop();           // 将空节点弹出
                node = stack.peek();    // 重新取出栈中元素
                stack.pop();
                list.add(node.val); // 加入到结果集
            }
        }
        return list;
   }

这个方法也是利用栈的迭代遍历方法,但是这种方法却实现了三种遍历的统一,它就像是第一种递归方法一样,在不同的遍历顺序中,只需要更改一个结点的入栈位置就可以了

另外,其实这个方法单纯看代码有点难想象和理解的,在这里呢,我们来看看下面这种理解:

  1. 在前序遍历中,入栈顺序为:右左中,而需要遍历的顺序为中左右
  2. 在中序遍历中,入栈顺序为:右中左,而需要遍历的顺序为左中右
  3. 在后序遍历中,入栈顺序为:中右左,而需要遍历的顺序为左右中

可以看到入栈顺序正好是遍历顺序的相反,按相反的顺序入栈,则出栈的时候就是正确的顺序,这也是栈的基本特性。