美文网首页
十二、二叉树的遍历

十二、二叉树的遍历

作者: 咸鱼Jay | 来源:发表于2022-01-21 09:26 被阅读0次

    简介

    • 遍历是数据结构中的常见操作
      把所有元素都访问一遍

    • 线性数据结构的遍历比较简单
      正序遍历
      逆序遍历

    • 根据节点访问顺序的不同,二叉树的常见遍历方式有4种
      前序遍历(Preorder Traversal)
      中序遍历(Inorder Traversal)
      后序遍历(Postorder Traversal)
      层序遍历(Level Order Traversal)

    前序遍历(Preorder Traversal)

    前序遍历(Preorder Traversal)
    访问顺序: \color{#ed7d30}{根节点}、前序遍历\color{#00afef}{左子树}、前序遍历\color{#00b050}{右子树}
    \color{#ed7d30}{7}、\color{#00afef}{4、2、1、3、5}、\color{#00b050}{9、8、11、10、12}

    前序遍历 – 递归

    public void preorderTraversal() {
        preorderTraversal(root);
    }
    
    private void preorderTraversal(Node<E> node) {
        if(node == null) return;
        System.out.println(node.element);
        preorderTraversal(node.left);
        preorderTraversal(node.right);
    }
    

    前序遍历 – 非递归

    前序遍历 – 非递归
    public void preorder(Visitor<E> visitor) {
        if(visitor == null || root == null) return;
        Node<E> node = root;
        Stack<Node<E>> stack = new Stack<>();
        while(true) {
            if(node != null) {
                if(visitor.visit(node.element)) return;
                // 将右子节点入栈
                if(node.right != null) {
                    stack.push(node.right);
                }
                // 向左走
                node = node.left;
            }else if(stack.isEmpty()) {
                return;
            }else {
                node = stack.pop();
            }
        }
    }
    

    非递归前序遍历的另一种思路

    非递归前序遍历的另一种思路
    public void preorder(Visitor<E> visitor) {
        if(visitor == null || root == null) return;
        Stack<Node<E>> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()) {
            Node<E> node = stack.pop();
            if(visitor.visit(node.element)) return;
            if(node.right != null) {
                stack.push(node.right);
            }
            if(node.left != null) {
                stack.push(node.left);
            }
        }
    }
    

    中序遍历(Inorder Traversal)

    中序遍历(Inorder Traversal)

    访问顺序: 中序遍历\color{#00afef}{左子树}\color{#ed7d30}{根节点}、中序遍历\color{#00b050}{右子树}
    \color{#00afef}{1、2、3、4、5}、\color{#ed7d30}{7}、\color{#00b050}{8、9、10、11、12}

    \color{red}{如果访问顺序是下面这样呢?}
    访问顺序:中序遍历\color{#00b050}{右子树}\color{#ed7d30}{根节点}、中序遍历\color{#00afef}{左子树}
    \color{#00b050}{12、11、10、9、8 }、\color{#ed7d30}{7}、\color{#00afef}{5、4、3、2、1}

    二叉搜索树的中序遍历特点:
    二叉搜索树的中序遍历结果是升序或者降序的

    中序遍历 – 递归

    public void inorderTraversal() {
        inorderTraversal(root);
    }
    
    private void inorderTraversal(Node<E> node) {
        if(node == null) return;
        inorderTraversal(node.left);
        System.out.println(node.element);
        inorderTraversal(node.right);
    }
    

    中序遍历 – 非递归

    中序遍历 – 非递归
    public void inorder(Visitor<E> visitor) {
        if (visitor == null || root == null) return;
        Node<E> node = root;
        Stack<Node<E>> stack = new Stack<>();
        while (true) {
            if (node != null) {
                stack.push(node);
                // 向左走
                node = node.left;
            } else if (stack.isEmpty()) {
                return;
            } else {
                node = stack.pop();
                // 访问node节点
                if (visitor.visit(node.element)) return;
                // 让右节点进行中序遍历
                node = node.right;
            }
        }
    }
    

    后序遍历(Postorder Traversal)

    后序遍历(Postorder Traversal)

    访问顺序: 后序遍历\color{#00afef}{左子树}、后序遍历\color{#00b050}{右子树}\color{#ed7d30}{根节点}
    \color{#00afef}{1、3、2、5、4}、\color{#00b050}{8、10、12、11、9}、\color{#ed7d30}{7}

    后序遍历 – 递归

    public void postorderTraversal() {
        postorderTraversal(root);
    }
    
    private void postorderTraversal(Node<E> node) {
        if(node == null) return;
        postorderTraversal(node.left);
        postorderTraversal(node.right);
        System.out.println(node.element);
    }
    

    后序遍历 – 非递归

    后序遍历 – 非递归
    public void postorder(Visitor<E> visitor) {
        if (visitor == null || root == null) return;
        Stack<Node<E>> stack = new Stack<>();
        stack.push(root);
        Node<E> prev = null;// 记录上一次弹出访问的节点
        while(!stack.isEmpty()) {
            Node<E> top =stack.peek();
            if(top.isLeaf() || (prev != null && prev.parent == top) ) {
                prev = stack.pop();
                if (visitor.visit(prev.element)) return;
            }else {
                if(top.right != null) {
                    stack.push(top.right);
                }
                if(top.left != null) {
                    stack.push(top.left);
                }
            }
        }
    }
    
    public void postorder(Visitor<E> visitor) {
        if (visitor == null || root == null) return;
        Stack<E> resStack = new Stack<>();
        Stack<Node<E>> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()) {
            Node<E> node = stack.pop();
            resStack.push(node.element);
            if(node.left != null) {
                stack.push(node.left);
            }
            if(node.right != null) {
                stack.push(node.right);
            }
        }
        while (!resStack.isEmpty()) {
            E element = resStack.pop();
            if (visitor.visit(element)) return;
        }
    }
    

    层序遍历(Level Order Traversal)

    层序遍历(Level Order Traversal)
    访问顺序: 从上到下、从左到右依次访问每一个节点
    \color{#ed7d30}{7}、\color{#00afef}{4、9}、\color{#00b050}{2、5、8、11}、1、3、10、12

    实现思路:使用队列

    1. 将根节点入队
    2. 循环执行以下操作,直到队列为空
      • 将队头节点 A 出队,进行访问
      • 将 A 的左子节点入队
      • 将 A 的右子节点入队
    public void levelOrderTraversal() {
        if(root == null) return;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        while(!queue.isEmpty()) {
            Node<E> node = queue.poll();
            System.out.println(node.element);
            if(node.left != null) {
                queue.offer(node.left);
            }
            if(node.right != null) {
                queue.offer(node.right);
            }
        }
    }
    

    设计遍历接口

    思考
    前面虽然写了前中后层序遍历,但是这写遍历,只是打印了节点数据,那么外界如何访问到这个节点数据呢?
    如果允许外界遍历二叉树的元素?你会如何设计接口?

    定义Visitor接口

    public static interface Visitor<E> {
        void visit(E element);
    }
    

    修改前序遍历

    public void preorder(Visitor<E> visitor) {
        preorder(root,visitor);
    }
    
    private void preorder(Node<E> node,Visitor<E> visitor) {
        if(node == null || visitor == null) return;
        visitor.visit(node.element);
        preorder(node.left,visitor);
        preorder(node.right,visitor);
    }
    

    修改中序遍历

    public void  inorder(Visitor<E> visitor) {
        inorder(root,visitor);
    }
    
    private void  inorder(Node<E> node,Visitor<E> visitor) {
        if(node == null || visitor == null) return;
        inorder(node.left,visitor);
        visitor.visit(node.element);
        inorder(node.right,visitor);
    }
    

    修改后序遍历

    public void  postorder(Visitor<E> visitor) {
         postorder(root,visitor);
    }
    
    private void  postorder(Node<E> node,Visitor<E> visitor) {
        if(node == null || visitor == null) return;
         postorder(node.left,visitor);
         postorder(node.right,visitor);
         visitor.visit(node.element);
    }
    

    修改层序遍历

    public void levelOrder(Visitor<E> visitor) {
        if(root == null || visitor == null) return;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        while(!queue.isEmpty()) {
            Node<E> node = queue.poll();
            visitor.visit(node.element);
            if(node.left != null) {
                queue.offer(node.left);
            }
            if(node.right != null) {
                queue.offer(node.right);
            }
        }
    }
    

    使用

    bst.preorder(new Visitor<Integer>() {
        @Override
        public void visit(Integer element) {
            System.out.print(element+" ");
        }
    });
    

    增强遍历接口

    现在提供了给外界遍历的接口,但是如果遍历的中间想停掉遍历,该怎么处理呢?
    修改Visitor接口里的方法和层序遍历


    但是这样修改了Visitor接口后,层序遍历可以完美的兼容,那么前、中、后序遍历就没法兼容了。

    这里拿中序遍历来举例:
    如果visitor.visit(node.element);的返回值用一个类似全局变量的记录一下,下次遍历的时候就可以根据上次的进行判断,但是如果直接使用全局遍历的话,在多线程操作下就会有问题。所以这里可以在Visitor里面新增一个变量进行使用就可以。

    遍历的应用

    • 前序遍历
      树状结构展示(注意左右子树的顺序)

    • 中序遍历
      二叉搜索树的中序遍历按升序或者降序处理节点

    • 后序遍历
      适用于一些先子后父的操作

    • 层序遍历
      计算二叉树的高度
      判断一棵树是否为完全二叉树

    根据遍历结果重构二叉树


    举例:


    计算二叉树的高度

    二叉树的高度其实就是根结点的高度。什么叫做高度呢?从当前结点开始一直到最远的叶子结点,所经历的结点数量就是高度。

    递归方法

    计算某个结点的高度其实就是看它左右结点的高度的最大值。

    public int height() {
        return height(root);
    }
    
    private int height(Node<E> node) {
        if(node == null) return 0;
        return 1 + Math.max(height(node.left),height(node.right));
    }
    

    非递归方法

    使用非递归的方法,就是迭代实现,可以用层序遍历来实现。
    这里使用层序遍历,当每访问一层就使局部变量height++,那我们怎么知道什么时候访问完一层呢?通过层序遍历的特点,可以知道每访问完一层,就可以通过队列知道下一层的结点数量。因此可以定义局部变量levelSize来表示每一层的结点数量。

    public int height() {
        if(root == null) return 0;
        
        //树的高度
        int height = 0;
        // 存储着每一层的元素数量
        int levelSize = 1;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        while(!queue.isEmpty()) {
            Node<E> node = queue.poll();
            levelSize--;//levelSize减为0,代表这一层就访问完了。
            
            if(node.left != null) {
                queue.offer(node.left);
            }
            if(node.right != null) {
                queue.offer(node.right);
            }
            
            if(levelSize == 0) {//意味着即将要访问下一层
                levelSize = queue.size();
                height++;
            }
        }
        return height;
    }
    

    判断一棵树是否为完全二叉树

    • 如果树为空,返回 false
    • 如果树不为空,开始层序遍历二叉树(用队列)
      • 如果node.left != null && node.right != null,将node.left、node.right按顺序入队
      • 如果node.left == null && node.right != null,返回false
      • 如果node.right!=null && node.right == null或者node.left == null && node.right != null
        • 那么后面遍历的节点应该都为叶子节点,才是完全二叉树
        • 否则返回false
      • 遍历结束,返回true

    这里为了方便实现,需要在Node结点类中新增加是否是叶子结点isLeaf方法,和是否有左右两个结点hasTwoChildren方法

    private static class Node<E> {
        E element;
        Node<E> left;
        Node<E> right;
        Node<E> parent;
        public Node(E element, Node<E> parent) {
            this.element = element;
            this.parent = parent;
        }
        
        // 是否是叶子结点
        public boolean isLeaf() {
            return left == null && right == null;
        }
        
        // 是否有左右两个结点
        public boolean hasTwoChildren() {
            return left != null && right != null;
        }
    }
    
    public boolean isComplete() {
        if(root == null) return false;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        boolean leaf = false;
        while(!queue.isEmpty()) {
            Node<E> node = queue.poll();
            if(leaf && !node.isLeaf()) {
                //如果要求你是叶子结点,但是你不是叶子结点
                return false;
            }
            
            if(node.hasTwoChildren()) {
                queue.offer(node.left);
                queue.offer(node.right);
            }else if (node.left == null && node.right != null) {
                return false;
            }else {// 后面遍历的结点都必须是叶子结点
                leaf = true;
                if(node.left != null) {
                    queue.offer(node.left);
                }
            }
        }
        return true;
    }
    

    下面我们简化代码逻辑

    • 如果树为空,返回 false
    • 如果树不为空,开始层序遍历二叉树(用队列)
      • 如果node.left!=null,将node.left入队
      • 如果node.left==null && node.right!=null,返回false
      • 如果node.right!=null,将node.right入队
      • 如果node.right==null
        • 那么后面遍历的节点应该都为叶子节点,才是完全二叉树
        • 否则返回false
      • 遍历结束,返回true
    public boolean isComplete() {
        if(root == null) return false;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        boolean leaf = false;
        while(!queue.isEmpty()) {
            Node<E> node = queue.poll();
            if(leaf && !node.isLeaf()) {
                //如果要求你是叶子结点,但是你不是叶子结点
                return false;
            }
            
            if(node.left != null) {
                queue.offer(node.left);
            }else if(node.right != null) {
                // node.left == null && node.right != null
                return false;
            }
            
            if(node.right != null) {
                queue.offer(node.right);
            }else {// node.right == null
                leaf = true;
            }
        }
        return true;
    }
    

    226. 翻转二叉树

    翻转一棵二叉树。

    示例:

    输入:

         4
       /   \
      2     7
     / \   / \
    1   3 6   9
    

    输出:

         4
       /   \
      7     2
     / \   / \
    9   6 3   1
    

    方法一:递归

    前序遍历

    public TreeNode invertTreePreorder(TreeNode root) {
        // 采用前序遍历
        if(root == null) return root;
        
        // 交换一下左右
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        
        invertTreePreorder(root.left);// 遍历左
        invertTreePreorder(root.right);// 遍历右
        return root;
    }
    

    中序遍历

    public TreeNode invertTreeInorder(TreeNode root) {
        // 采用中序遍历
        if(root == null) return root;
        
        invertTreeInorder(root.left);
        
        // 交换一下左右
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        
        invertTreeInorder(root.left);// 注意这里不是right了,
        return root;
    }
    

    后序遍历

    public TreeNode invertTreePostorder(TreeNode root) {
        // 采用后序遍历
        if(root == null) return root;
        
        invertTreePostorder(root.left);// 遍历左
        invertTreePostorder(root.right);// 遍历右
        
        // 交换一下左右
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        return root;
    }
    

    方法二:迭代

    层序遍历

    public TreeNode invertTreeLevelOrder(TreeNode root) {
        // 采用层序遍历
        if(root == null) return root;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        
        while(!queue.isEmpty()) {
            TreeNode node = queue.poll();
            
            // 交换一下左右
            TreeNode tmp = node.left;
            node.left = node.right;
            node.right = tmp;
            
            if(node.left != null) {
                queue.offer(node.left);
            }
            if(node.right != null) {
                queue.offer(node.right);
            }
        }
        return root;
    }
    

    相关文章

      网友评论

          本文标题:十二、二叉树的遍历

          本文链接:https://www.haomeiwen.com/subject/huthhrtx.html