【树】力扣144:二叉树的前序遍历(附中序后序层序)

输入一个二叉树,输出一个数组,为二叉树前序遍历的结果。

示例:

image
输入:root = [1,null,2,3]
输出:[1,2,3]

二叉树遍历题目:

114. 二叉树的前序遍历
94. 二叉树的中序遍历
145. 二叉树的后序遍历
102. 二叉树的层序遍历

主要参考:https://leetcode.cn/problems/binary-tree-preorder-traversal/solution/tu-jie-er-cha-shu-de-si-chong-bian-li-by-z1m/

递归

递归的本质是栈的调用。

递归的模板相对比较固定,一般都会新增一个 dfs 函数:

def dfs(root):
    if not root:
        return None

    res.append(root.val) # 前序遍历
    dfs(root.left)
    dfs(root.right)

对于前序、中序和后序遍历,只需将递归函数里的 res.append(root.val) 放在不同位置即可,然后在主函数中调用这个递归函数就可以了,代码完全一样。

前序

  1. base case: 空树,即根结点为空

  2. 重复的子问题:先取根结点,再遍历左子树,最后遍历右子树

  3. 终止条件:当前结点为空

定义辅函数 preorder(node) 表示当前遍历到 node 结点的结果集。先将 node 结点的值加入结果集,然后递归调用 preorder(node.left) 来遍历 node 结点的左子树,再递归调用 preorder(node.right) 来遍历 node 结点的右子树。递归终止的条件为结点为空。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root: # base case
            return []

        res = []
        def preorder(node):
            if not node: # 递归终止条件
                return None
            res.append(node.val) # 将 node 结点的值加入结果集
            preorder(node.left) # 遍历 node 结点的左子树
            preorder(node.right) # 遍历 node 结点的右子树

        preorder(root)
        return res

单独辅函数版本,注意self

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        self.preorder(root)

    def preorder(self, root): # 加上 self
        if root == None:
            return
        res.append(root.val)
        self.preorder(root.left)
        self.preorder(root.right)

时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。

空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

中序

遍历顺序:左孩子 -> 根结点 -> 右孩子

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def dfs(root):
            if not root:
                return None
            dfs(root.left)
            res.append(root.val)
            dfs(root.right)
        dfs(root)
        return res

后序

遍历顺序:左孩子 -> 右孩子 -> 根结点

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def dfs(root):
            if not root:
                return None
            dfs(root.left)
            dfs(root.right)
            res.append(root.val)
        dfs(root)
        return res

迭代

递归是隐式地维护了一个栈,在迭代的时候就需要显式地将这个栈模拟出来。

迭代法就是不断地将旧的变量值,递推计算新的变量值。

前序

1. 常规解法

前序遍历的顺序是 根结点、左子树、右子树,也就是 先输出根结点,再输出左子树,最后输出右子树。

由于栈 “先入后出” 的特点,所以结合前序遍历的顺序,迭代的过程就是:每次都先将根结点放入栈,然后是右子树和左子树。

image

  • 初始化维护一个栈 stack,先将根结点 root 入栈
    stack = [root]

  • 当栈不为空时

    • 弹出栈顶元素 node,将 node 结点的值加入结果集 res 中
    node = stack.pop()
    res.append(node.val)
    
    • 若 node 的右子树不为空,右子树入栈
    if node.right:
        stack.append(node.right)
    
    • 若 node 的左子树不为空,左子树入栈
    if node.left:
        stack.append(node.left)
    

总体代码:

能够理解循环内的 node 不是 树的 root,也可以把 node 写为 root,在循环内这只是一个变量。

实际上就是dfs的实现

class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []

        stack, res = [root], []
        while stack:
            node = stack.pop()
            if node: # 可以不要这句判断
                res.append(node.val)
                if node.right:
                    stack.append(node.right)
                if node.left:
                    stack.append(node.left)
        return res

时间复杂度:O(n),其中 n 是二叉树的节点数。每一个结点恰好被遍历一次。

空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

这里的stack = [root],也可以写成

2. 模板解法

模板解法的思路稍有不同:

  • 先将根结点 cur 和所有的左孩子入栈,并加入到结果集中,直至 cur 为空,用一个 while 循环实现

  • 然后,每弹出一个栈顶元素 tmp,就到达它的右孩子,再将这个结点当作 cur 重新按上面的步骤来一遍,直至栈为空。这里又需要一个 while 循环

class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []

        cur, stack, res = root, [], []
        while cur or stack:
            while cur: # 根结点和所有左孩子入栈,同时添加到结果集
                stack.append(cur)
                res.append(cur.val)
                cur = cur.left
            # 每弹出一个元素,就到达其右孩子
            tmp = stack.pop()
            cur = tmp.right

        return res

中序

遍历顺序:左孩子 -> 根结点 -> 右孩子

和前序遍历的模板代码完全相同,只是在出栈的时候才将结点 tmp 的值加入到结果中。

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []

        cur, stack, res = root, [], []
        while cur or stack:
            while cur: # 根结点和所有左孩子入栈
                stack.append(cur)
                cur = cur.left
            tmp = stack.pop()
            res.append(tmp.val) # 出栈时再加入结点 tmp 的值
            cur = tmp.right

        return res

后序

模板解法

  1. 结点 cur 先到达最右端的叶子节点,并将路径上的结点入栈。

  2. 每次从栈中弹出一个元素后,cur 到达它的左孩子,并将左孩子看作 cur 继续执行上面的步骤。

  3. 最后将结果反向输出即可。

总体来讲就是和前序遍历的左右相反。

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []

        cur, stack, res = root, [], []
        while cur or stack:
            while cur: # 从根结点开始向右下遍历,先到达最右端叶子结点
                stack.append(cur)
                res.append(cur.val)
                cur = cur.right
            tmp = stack.pop()
            cur = tmp.left

        return res[::-1] # 反向输出

后序遍历采用模板解法并没有按照真实的栈操作,而是利用了结果的特点反向输出,显得技术含量不足。因此掌握标准的栈操作解法是必要的。

常规解法

类比前序遍历的常规解法,只需要将输出的“根 -> 左 -> 右”的顺序改为“左 -> 右 -> 根”就可以了。

如何实现呢?

有一个小技巧:入栈时额外加入一个标识,比如在栈中使用 flag == 0。每次从栈中弹出元素时,如果 flag 为 0,则需要将 flag 变为 1,并连同该结点再次入栈,只有当 flag 为 1 时才可将该结点加入到结果集中。

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []

        stack, res = [(0, root)], [] # 在栈中添加 flag == 0,用小括号()
        while stack:
            flag, node = stack.pop()
            if not node:
                continue
            if flag == 1: # 遍历过了,加入到结果集 res 中
                res.append(node.val)
            else:
                stack.append((1, node))
                # 先右再左
                stack.append((0, node.right))
                stack.append((0, node.left))
        return res

层序

二叉树的层次遍历的迭代方法与前面不用,因为前面的都采用的是深度优先搜索的方式,而层次遍历使用的是广度优先搜索,广度优先搜索主要使用队列实现,也就不能使用前面的模板解法了。

广度优先搜索的步骤:

  • 初始化队列 queue,并将根结点 root 加入到队列中

  • 当 queue 不为空时:

    • 从 queue 中弹出结点 node,加入到结果中

    • 如果左子树非空,左子树加入 queue

    • 如果右子树非空,右子树加入queue

此外,由于题目要求每一层保存在一个子数组中,所以额外加入变量 level 保存每层的遍历结果,并使用 for 循环来实现

image

class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []

        queue, res = [root], []
        while queue:
            level = []
            for i in range(len(queue)):
                node = queue.pop(0) # 这里的 queue 相当于一个队列,一端进另一端出。不是pop(),是pop(0)
                level.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            res.append(level)
        return res

Morris 中序遍历(常数空间)

用递归和迭代的方式都使用了辅助的空间,而莫里斯遍历的优点是没有使用任何辅助空间。

缺点是改变了整个树的结构,强行把一棵二叉树改成一段链表结构。

image
相当于将黄色区域部分挂到结点值为 5 的右子树上,接着再把 2 和 5 对应的这两个结点挂到 4 的右边。

这样整棵树基本上就变改成了一个链表了,之后再不断往右遍历。

image

前置知识:

  • 前驱结点:如果按照中序遍历访问树,访问的结果为 ABC,则称 A 为 B 的前驱结点,B 为 C 的前驱结点

  • 前驱结点 predecessor(简写为pred)是 cur 左子树的最右子树(按照中序遍历走一遍就知道了)

  • 由此可知,前驱结点的右子结点一定为空

主要思想:

  • 树的链接是单向的,从根结点出发,只有通往子结点的单向路程

  • 中序遍历迭代法的难点就在于:必须先访问当前结点的左子树,才能访问当前结点。但是只有通往左子树的单向路程,而没有回程路,因此无法进行下去,除非用额外的数据结构记录下回程的路

  • 在这里可以利用当前结点的 前驱结点 建立回程的路,也不需要消耗额外的空间

  • 根据前置知识的分析,当前结点的前驱结点的右子结点为空,因此可以用其保存回程的路

  • 但是要注意,这是建立在破坏了树的结构的基础上的,因此最后还有一步“消除链接”的步骤,将树的结构还原

重点过程:当遍历到当前结点 cur 时,使用 cur 的前驱结点 pred

  • 标记当前结点是否访问过

  • 记录回溯到 cur 的路径(访问完 pred 以后,就应该访问 cur 了)

访问 cur 结点需要做的事:

  • 访问 cur 结点的时候,先找其前驱结点 pred

  • 找到前驱结点 pred 后,根据其右指针的值,来判断 cur 的访问状态

    • 若 pred 的右子结点为空,说明 cur 第一次访问,其左子树还没有访问,此时应该将其指向cur,并访问 cur 的左子树

    • 若 pred 的右子结点指向 cur,那么说明这是第二次访问 cur 了,也就是说其左子树已经访问完了,此时将 cur.val 加入结果集中

class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        if not root:
            return[]

        res = []
        pred, cur = None, root # 初始化前驱结点和当前结点(因为操作从 root 开始,也可以直接用 root 表示当前结点)
        while cur:
            # 若左结点不为空,就将当前结点连带右子树全部挂到左结点的最右子树下面
            if cur.left:
                pred = cur.left
                while pred.right: # 右结点为空时说明到达了左子树的最右结点
                    pred = pred.right
                pred.right = cur # 当前结点变为前驱结点的子结点
                tmp = cur
                cur = cur.left # 新的当前结点作为形状上的树的根结点
                tmp.left = None
            # 若左子树为空,则将这个结点加入结果集,并向右边遍历,一边输出一边检查是否所有结点都没有了左子树。如果有,则继续操作,没有则输出
            else:
                res.append(cur.val)
                cur = cur.right
        return res

作者:wang_ni_ma
链接:https://leetcode.cn/problems/binary-tree-inorder-traversal/solution/dong-hua-yan-shi-94-er-cha-shu-de-zhong-xu-bian-li/

时间复杂度:O(n),其中 n 为树的结点个数。找到每个前驱结点的复杂度是 O(n),因为 n 个结点的二叉树有 n − 1 条边,每条边只可能使用 2 次(一次定位到结点,一次找到前驱结点),因此总时间复杂度为 O(2n) = O(n)。

空间复杂度:O(1)。

posted @ 2022-07-20 18:44  Vonos  阅读(143)  评论(0)    收藏  举报