十二、二叉树的遍历

简介

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

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

  • 根据节点访问顺序的不同,二叉树的常见遍历方式有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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容