LeetCode94题:二叉树的中序遍历(python3)

在这里插入图片描述

二叉树知识总结:
DFS和BFS异同:
二叉树深度遍历是一个非常经典的问题,也是二叉树和递归的基础,必须掌握。
所谓二叉树遍历(traversal)指的是按照一定次序系统地访问一棵二叉树,使每个节点恰好被访问一次。
二叉树遍历实质上是二叉树的线性化,将树状结构变为线性结构。

二叉树遍历有两大类:

**深度优先(depth first traversal,DFS):**先完成一棵子树的遍历再完成另一棵
**广度优先(breath first traversal,BFS):**先完成一层节点的遍历再完成下一层
DFS:优先移动节点,当对给定节点尝试过每一种可能性之后,才退到前一节点来尝试下一个位置。
BFS:优先对给定节点的下一个位置进行进行尝试,当对给定节点尝试过每一种可能性之后,才移动到下一个节点。

方法一:递归遍历
我们先递归左子树,再访问根节点,接着递归右子树。

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

时间复杂度 O(n),空间复杂度 O(n)。其中 n 是二叉树的节点数,空间复杂度主要取决于递归调用的栈空间。

方法二:栈实现非递归遍历
非递归的思路如下:
定义一个栈 stk
将树的左节点依次入栈
左节点为空时,弹出栈顶元素并处理
重复 2-3 的操作

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans, stk = [], []
        while root or stk:
            if root:
                stk.append(root)
                root = root.left
            else:
                root = stk.pop()
                ans.append(root.val)
                root = root.right
        return ans

时间复杂度 O(n),空间复杂度 O(n)。其中 n 是二叉树的节点数,空间复杂度主要取决于栈空间。

递归写法
深度优先遍历又分为

前序遍历:访问根节点的值->前序遍历左子树->前序遍历右子树 ABDEGCF
中序遍历:中序遍历左子树->访问根节点的值->中序遍历右子树 DBGEACF
后序遍历:后序遍历左子树->后序遍历右子树->访问根节点的值 DGEBFCA

# 前序:访问根->遍历左->遍历右
def preorder(cur_node):
    if cur_node is None:
        return
    visit(cur_node)
    preorder(cur_node.left)
    preorder(cur_node.right)

# 中序:遍历左->访问根->遍历右
def inorder(cur_node):
    if cur_node is None:
        return
    inorder(cur_node.left)
    visit(cur_node)
    inorder(cur_node.right)

# 后序:遍历左->遍历右->访问根
def postorder(cur_node):
    if cur_node is None:
        return
    postorder(cur_node.left)
    postorder(cur_node.right)
    visit(cur_node)

其中函数visit(node)表示访问node这个节点的值。更具体来说,可以是print(node.val)或者是将node.val的值存入全局数组等操作,这个根据具体问题决定。

注意到,不管是哪一种DFS,递归函数都包含:

同样的递归终止条件:当遇到当前节点cur_node为空节点,终止递归
两次递归函数调用:一次对左孩子cur_node.left进行递归,一次对右孩子cur_node.right进行递归
一次访问当前节点:即visit(cur_node)的调用
而三种DFS的最大区别仅仅在于,访问根节点、遍历左孩子、遍历右孩子这三个函数的前后顺序不同。
因此只需要一套模板,就可以很方便地记住前序/中序/后序这三种不同的DFS的代码了。

非递归写法(迭代写法)
递归带来大量函数调用和额外的时间。理论上,所有递归算法都可以用非递归的迭代写法来实现。

递归执行的过程中,会使用编译栈来储存每一个递归函数的地址。

因此我们可以仿照编译栈的工作原理,显式地将节点储存在栈中。

这跟二叉树的层序遍历是非常雷同的。我们可以非常容易地写出如下所示的前序遍历的代码

# LeetCode144题二叉树的前序遍历,迭代写法(非模板)
class Solution:
    def preorderTraversal(self, root):
        if root is None:
            return []
        ans = list()
        # 初始化栈为包含root节点
        stack = [root]
        # 进行DFS
        while stack:
            # 弹出栈顶元素
            node = stack.pop()
            # 注意这里的压栈顺序是,右入栈->左入栈
            # 这样的出栈顺序才会是,先node.left再node.right
            if node.right:
                stack.append(node.right)
            if node.left:
                stack.append(node.left)
            # 这访问node节点的值,写在哪里都可以
            ans.append(node.val)
        return ans

但是,想写出类似的中序和后序遍历的代码,就会发现有点困难了。因为visit(node)这一步无论放在哪个位置,都不会影响这是一个前序遍历的结果。

注意到,在前面的递归写法中,我们是将遍历(traverse)和访问(visit)这两个概念区分开的。以中序遍历的伪代码为例

# 中序:遍历左->访问根->遍历右
# 当前节点cur_node的遍历(递归入口)   
def inorder(cur_node):
    ...
    inorder(cur_node.left)            # 左孩子cur_node.left的遍历
    visit(cur_node)                   # 当前节点cur_node的访问                
    inorder(cur_node.right)           # 右孩子cur_node.right的遍历

遍历表示要根据该节点去考虑其子节点的情况,而访问表示要访问这个节点的val。
显然这两者的行为是不完全一致的,对于当前节点cur_node而言,cur_node的遍历包含了以下三部分:
左孩子cur_node.left的遍历
当前节点cur_node的访问
右孩子cur_node.right的遍历
换句话说,我们在遍历cur_node的时候,不能立刻对cur_node进行访问,而应该先对cur_node.left进行遍历之后(如果是后序遍历,还得是在对cur_node.right进行访问之后),才能对cur_node进行访问。

但这里就出现了一个问题:在cur_node.left遍历结束后,我们如何才能够回到cur_node来访问cur_node的值?

在递归写法中,由于cur_node.left的遍历是通过递归调用来执行的,在关于cur_node.left的递归调用结束后,会自然地回到当前关于cur_node的遍历中,其下一行要执行的内容自然就是visit(cur_node)。

但这并不会在迭代写法中自然出现。我们必须把遍历和访问的区别,用一个标识符f显式地标记出来。譬如

f = 0表示这个节点有待遍历
f = 1表示这个节点有待访问
在每一个节点入栈时,不仅要储存这个节点本身,还需要储存这个节点下一步的行为对应的标识符f。

而每一个节点出栈时,我们也会根据标识符f来区分此时节点对应的行为是遍历还是访问。

# LeetCode94题二叉树的中序遍历,迭代写法(模板)
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = list()
        stack = list()
        # 入栈除了节点之外,还储存了一个标记f
        # 标记f指的是,我们对节点要做不同的事情:遍历还是访问
        # f = 0表示这个节点有待遍历
        # f = 1表示这个节点有待访问
        # 注意辨析这里的遍历和访问的区别
        # 遍历traverse  表示要根据该节点去考虑其子节点的情况
        # 访问visit     表示要访问这个节点的val
        if root:
            stack.append([root, 0])
        while(stack):
            node, f = stack.pop()
            # 如果当前f为0.对应遍历node的操作
            if f == 0:
                # 注意压栈顺序和中序遍历的顺序必须相反
                # 即:右有待遍历->根有待访问->左有待遍历
                # 到时出栈的顺序就会是:
                # 遍历左->访问根->遍历右
                # 如果是前序或后序遍历,仅需修改以下三行的顺序即可0
                if node.right: stack.append([node.right, 0])
                stack.append([node, 1])
                if node.left:  stack.append([node.left, 0])
            # 如果当前f为1,对应访问node的操作,将node.val加入全局答案变量中
            if f == 1:
                ans.append(node.val)
        return ans

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值