十二、二叉树的遍历

简介

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

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

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

前序遍历(Preorder Traversal)

前序遍历(Preorder Traversal)

访问顺序: 、前序遍历 、前序遍历

前序遍历 – 递归

public void preorderTraversal() {
    preorderTraversal(root);
}

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

前序遍历 – 非递归

前序遍历 – 非递归
public void preorder(Visitor visitor) {
    if(visitor == null || root == null) return;
    Node node = root;
    Stack> 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 visitor) {
    if(visitor == null || root == null) return;
    Stack> stack = new Stack<>();
    stack.push(root);
    while(!stack.isEmpty()) {
        Node 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)

访问顺序: 中序遍历、、中序遍历


访问顺序:中序遍历、、中序遍历

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

中序遍历 – 递归

public void inorderTraversal() {
    inorderTraversal(root);
}

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

中序遍历 – 非递归

中序遍历 – 非递归
public void inorder(Visitor visitor) {
    if (visitor == null || root == null) return;
    Node node = root;
    Stack> 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)

访问顺序: 后序遍历、后序遍历、

后序遍历 – 递归

public void postorderTraversal() {
    postorderTraversal(root);
}

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

后序遍历 – 非递归

后序遍历 – 非递归
public void postorder(Visitor visitor) {
    if (visitor == null || root == null) return;
    Stack> stack = new Stack<>();
    stack.push(root);
    Node prev = null;// 记录上一次弹出访问的节点
    while(!stack.isEmpty()) {
        Node 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 visitor) {
    if (visitor == null || root == null) return;
    Stack resStack = new Stack<>();
    Stack> stack = new Stack<>();
    stack.push(root);
    while(!stack.isEmpty()) {
        Node 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)

访问顺序: 从上到下、从左到右依次访问每一个节点

实现思路:使用队列

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

修改前序遍历

public void preorder(Visitor visitor) {
    preorder(root,visitor);
}

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

修改中序遍历

public void  inorder(Visitor visitor) {
    inorder(root,visitor);
}

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

修改后序遍历

public void  postorder(Visitor visitor) {
     postorder(root,visitor);
}

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

修改层序遍历

public void levelOrder(Visitor visitor) {
    if(root == null || visitor == null) return;
    Queue> queue = new LinkedList<>();
    queue.offer(root);
    
    while(!queue.isEmpty()) {
        Node 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() {
    @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 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> queue = new LinkedList<>();
    queue.offer(root);
    
    while(!queue.isEmpty()) {
        Node 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 element;
    Node left;
    Node right;
    Node parent;
    public Node(E element, Node 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> queue = new LinkedList<>();
    queue.offer(root);
    
    boolean leaf = false;
    while(!queue.isEmpty()) {
        Node 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> queue = new LinkedList<>();
    queue.offer(root);
    
    boolean leaf = false;
    while(!queue.isEmpty()) {
        Node 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 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;
}

你可能感兴趣的:(十二、二叉树的遍历)