【算法】二叉树遍历算法总结:前序中序后序遍历

前言

二叉树遍历是非常经典的算法题,也是二叉树的一道基础算法题。

但是在平常的笔试面试中,其出现的频率其实并不是特别的高,我推测是这种题目相对来说比较基础,算是一个基础知识点。

比如剑指offer中出现的后序遍历题目,是给出一个数字序列,让你判断是不是平衡二叉树后序遍历序列,这样出题的难度比直接让你写后序遍历难很多。

但是,二叉树遍历容易吗?在递归方法下,前中后序遍历都是一个思路,理解起来也比较容易。

但是只是用迭代的话,二叉树遍历其实是有难度的!,这也是为什么LeetCode会在这三题题目的下方写出进阶: 递归算法很简单,你可以通过迭代算法完成吗?这句话了。

本文的主要内容如下:

  • 题目定义
    • 上篇:二叉树前序、中序、后序遍历
    • 下篇:层序遍历、其他遍历相关题型
  • 解题思路:递归 + 迭代+ 莫里斯Morris遍历
  • 解题代码:Java + Python

注1:本文中的解题思路会尽量的全面,但是解题方法千变万化,有很多奇技淫巧我不会去介绍,大家有兴趣可以自行扩展学习。

注2:本文中的代码会尽量简单,易懂,并不会去追求极致的写法(比如:在一行内完成,使用各种非正式的库等)。

正文

二叉树的定义

最多有两棵子树的树被称为二叉树

image

二叉树下还有很多种特殊的二叉树,下方简单介绍几种常用的。

满二叉树

二叉树中所有非叶子结点的度都是2,且叶子结点都在同一层次上

image

完全二叉树(可以不满)

如果一个二叉树与满二叉树前m个节点的结构相同,这样的二叉树被称为完全二叉树。也就是说,如果把满二叉树从右至左、从下往上删除一些节点,剩余的结构就构成完全二叉树。

image

二叉搜索树

二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树
image

二叉树前中后序遍历

遍历方法

前序遍历:根结点 ---> 左子树 ---> 右子树

中序遍历:左子树---> 根结点 ---> 右子树

后序遍历:左子树 ---> 右子树 ---> 根结点

题目介绍

前序遍历

LeetCode题目地址:

https://leetcode-cn.com/problems/binary-tree-preorder-traversal/

输入: [1,null,2,3]  
   1
    \
     2
    /
   3 

输出: [1,2,3]

中序遍历

LeetCode题目地址:

https://leetcode-cn.com/problems/binary-tree-inorder-traversal/

输入: [1,null,2,3]
   1
    \
     2
    /
   3

输出: [1,3,2]

后序遍历

LeetCode题目地址:

https://leetcode-cn.com/problems/binary-tree-postorder-traversal/

输入: [1,null,2,3]  
   1
    \
     2
    /
   3 

输出: [3,2,1]

解题思路详解与代码实现

二叉树的前中后序遍历,主要就是两种思路,一种是递归,一种是迭代。

如果看到这里还没有感觉,不用担心,先直接往下看,第一个代码(前序遍历的递归思路)会帮助你提升感觉。

递归思路

递归思路是最容易理解的思路,并且前中后序遍历都相同。

比如前序遍历,在递归的函数里,先往结果数组里加入根节点,然后加入根节点的左节点,然后加入根节点的右节点。最后所有递归的函数运行完毕,结果集就已经完成了。

中序和后序的思路相同,就不再赘述了。

前序遍历

Java:

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
            List<Integer> result = new ArrayList<>();
            if (root == null) {
                return result;
            }
            preorder(root, result);
            return result;
        }
    
    private static List<Integer> preorder(TreeNode root, List<Integer> result) {
        if (root != null) {
            result.add(root.val);
            preorder(root.left, result);
            preorder(root.right, result);
        }
        return result;
    }
}

Python:

class Solution(object):
    def _preorderTraversal(self, root, result):
        if root:
            result.append(root.val)
            self._preorderTraversal(root.left, result)
            self._preorderTraversal(root.right, result)
    def preorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if root == None:
            return []
        result = []
        self._preorderTraversal(root, result)
        return result
中序遍历

Java:

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) {
            return result;
        }
        result = inorder(root, result);
        return result;
    }

    private static List<Integer> inorder(TreeNode root, List<Integer> result) {
        if (root != null) {
            inorder(root.left, result);
            result.add(root.val);
            inorder(root.right, result);
        }
        return result;
    }
}

Python:

class Solution(object):
    def generate(self, root, result):
        if root:
            self.inorder(root.left, list)
            result.append(root.val)
            self.inorder(root.right, list)
    def inorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if not root:
            return []
        result = []
        self.generate(root, result)
        return result
后序遍历

Java:

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) {
            return result;
        }
        result = postorder(root, result);
        return result;
    }

    private static List<Integer> postorder(TreeNode root, List<Integer> result) {
        if (root != null) {
            postorder(root.left, result);
            postorder(root.right, result);
            result.add(root.val);
        }
        return result;
    }
}

Python:

class Solution(object):
    
    def _postorderTraversal(self, root, result):
        if root:
            self._postorderTraversal(root.left, result)
            self._postorderTraversal(root.right, result)
            result.append(root.val)
    def postorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if root == None:
            return []
        result = []
        self._postorderTraversal(root, result)
        return result

迭代思路

前序遍历

我们需要一个栈来完成遍历。

1.根节点入栈

2.取出节点,值加入结果,然后先加右,后加左。

3.重复2

这样就得到了 根节点——左子树——右子树 的遍历结果集。

Java:

来自官方题解

LinkedList<TreeNode> stack = new LinkedList<>();
    LinkedList<Integer> output = new LinkedList<>();
    if (root == null) {
      return output;
    }

    stack.add(root);
    while (!stack.isEmpty()) {
      TreeNode node = stack.pollLast();
      output.add(node.val);
      if (node.right != null) {
        stack.add(node.right);
      }
      if (node.left != null) {
        stack.add(node.left);
      }
    }
    return output;
  }

Python:

class Solution(object):
    def preorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        ret = []
        stack = [root]
        while stack:
            node = stack.pop()
            if node:
                ret.append(node.val)
                if node.right:
                    stack.append(node.right)
                if node.left:
                    stack.append(node.left)
        return ret
中序遍历

还是使用一个栈来解决问题。

步骤如下:

                     1

          /   \

           2    3

           /   \  /   \

         4     5  6    7
  1. 我们将根节点1入栈,如果有左孩子,依次入栈,那么入栈顺序为:1,2,4。由于4的左子树为空,停止入栈,此时栈为{1,2,4}。

  2. 此时将4出栈,并遍历4,由于4也没有右孩子,那么根据中序遍历的规则,我们显然应该继续遍历4的父亲2,情况是这样。所以我们继续将2出栈并遍历2,2存在右孩子,将5入栈,此时栈为{1,5}。

  3. 5没有孩子,则将5出栈并遍历5,这也符合中序遍历的规则。此时栈为{1}。

  4. 1有右孩子,则将1出栈并遍历1,然后将右孩子3入栈,并继续以上三个步骤即可。

栈的变化过程:{1}->{1,2}->{1,2,4}->{1,2}->{1}->{1,5}->{1}->{}->{3}->{3,6}->{3}->{}->{7}->{}。

总结:从根节点遍历,先放入所有有左孩子的节点直到没有,然后出栈,出栈的时候就将出栈的数字放入结果集,然后看其有没有右孩子,有的话右孩子入栈。

Java:

官方题解

public class Solution {

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

Python:

class Solution:
    # @param root, a tree node
    # @return a list of integers
    def inorderTraversal(self, root):
        result = []
        stack = []
        while root or stack:
            if root:
                stack.append(root)
                root = root.left
            else:
                root = stack.pop()
                result.append(root.val)
                root = root.right
        return result
后序遍历

将数组输出为右子树-左子树-根节点。最后,再将数组倒序输出,形成后序遍历。这样代码并不用很繁琐,也能做完迭代。

是不是似曾相识,没错,和前序遍历的迭代几乎一样,仅仅是先放右节点再放左节点变成了先放左节点再放右节点,然后倒序输出。

Java:

class Solution {
  public List<Integer> postorderTraversal(TreeNode root) {
    LinkedList<TreeNode> stack = new LinkedList<>();
    LinkedList<Integer> output = new LinkedList<>();
    
    if (root == null) {
      return output;
    }

    stack.add(root);
    while (!stack.isEmpty()) {
      TreeNode node = stack.pollLast();
      output.addFirst(node.val);
      if (node.left != null) {
        stack.add(node.left);
      }
      if (node.right != null) {
        stack.add(node.right);
      }
    }
    return output;
  }
}

Python:

class Solution(object):
    def postorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if root is None:
            return []
        ret = []
        stack = [root]
        while stack:
            node = stack.pop()
            if node:
                ret.append(node.val)
                if node.left:
                    stack.append(node.left)
                if node.right:
                    stack.append(node.right)
        return ret[::-1]

所以迭代方式,前后序是非常类似的,中序遍历可能需要单独理解下。

莫里斯遍历

二叉树常规的遍历方法是用递归来实现的,这种方法一般都需要O(n)的空间复杂度和O(n)的时间复杂度。而Morris方法实现的是O(1)的空间复杂度和O(n)的时间复杂度。

我们知道,遍历二叉树时,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。(不然就是普通迭代方法)。

为了解决这个问题,Morris方法用到了线索二叉树(threaded binary tree)的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱(predecessor)和后继节点(successor),只需要利用叶子节点中的左右空指针指向某种顺序遍历下的前驱节点或后继节点就可以了

听不懂没关系,下面使用中序遍历作为例子来理解莫里斯遍历到底是什么意思,例子来自LeetCode官方题解:

中序遍历
Step 1: 将当前节点current初始化为根节点
Step 2: While current不为空,

若current没有左子节点
   a. 将current添加到输出
   b. 进入右子树,亦即, current = current.right
否则
   a. 在current的左子树中,令current成为最右侧节点的右子节点
   b. 进入左子树,亦即,current = current.left
      1
    /   \
   2     3
  / \   /
 4   5 6

首先,1 是根节点,所以将 current 初始化为 1。1 有左子节点 2,current 的左子树是

     2
    / \
   4   5

在此左子树中最右侧的节点是 5,于是将 current(1) 作为 5 的右子节点。令 current = cuurent.left (current = 2)。 现在二叉树的形状为:

 2
/ \
4   5
    \
     1
      \
       3
      /
     6

对于 current(2),其左子节点为4,我们可以继续上述过程

4
 \
  2
   \
    5
     \
      1
       \
        3
       /
      6

Java:

class Solution {
    public List < Integer > inorderTraversal(TreeNode root) {
        List < Integer > res = new ArrayList < > ();
        TreeNode curr = root;
        TreeNode pre;
        while (curr != null) {
            if (curr.left == null) {
                res.add(curr.val);
                curr = curr.right; // move to next right node
            } else { // has a left subtree
                pre = curr.left;
                while (pre.right != null) { // find rightmost
                    pre = pre.right;
                }
                pre.right = curr; // put cur after the pre node
                TreeNode temp = curr; // store cur node
                curr = curr.left; // move cur to the top of the new tree
                temp.left = null; // original cur left be null, avoid infinite loops
            }
        }
        return res;
    }
}
前序遍历

理解了中序遍历,前序和后序遍历相对来说也就更容易理解了。所以前序和后序贴了思路,代码我也没自己写后submit,在这里就不放了。

算法的思路是从当前节点向下访问先序遍历的前驱节点,每个前驱节点都恰好被访问两次。

首先从当前节点开始,向左孩子走一步然后沿着右孩子一直向下访问,直到到达一个叶子节点(当前节点的中序遍历前驱节点),所以我们更新输出并建立一条伪边 predecessor.right = root 更新这个前驱的下一个点。如果我们第二次访问到前驱节点,由于已经指向了当前节点,我们移除伪边并移动到下一个顶点。

后序遍历

后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。

步骤:

当前节点设置为临时节点dump。

  1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。

  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。

  3. 重复以上1、2直到当前节点为空。

参考

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

推荐阅读更多精彩内容