二叉树迭代&递归遍历练习大全

📊知识点熟悉


*二叉树定义*

每个节点最多有两个子节点的树结构。子节点通常被称为左子节点(left child)和右子节点(right child)。

*二叉树主要有两种遍历方式*

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
  2. 广度优先遍历:一层一层的去遍历。

*深度优先遍历*

  • 前序遍历(递归法,迭代法均可)
  • 中序遍历(递归法,迭代法均可)
  • 后序遍历(递归法,迭代法均可)

*广度优先遍历*

  • 层次遍历(只能迭代法)

*递归法*

概念

  • 递归(Recursion)是指函数调用自身的一种编程技巧。基本思想是将问题分解为更小的相同问题,直至遇到基准情况(Base Case),递归停止。

特点

  • 代码简洁:递归代码通常更简洁、易读。
  • 自然表达:递归与树的结构匹配,树的节点本身就是递归定义的。
  • 基准情况:递归必须有明确的停止条件,否则会导致无限递归。
  • 系统调用栈:递归利用系统调用栈保存函数状态。

优点

  • 易于实现和理解。
  • 更符合树的自然定义。

缺点

  • 栈溢出风险:深度较大的树可能导致栈溢出。
  • 性能开销:递归调用有一定的函数调用开销。

应用

  • 遍历(前序、中序、后序)。
  • 查找节点。
  • 修改节点。
  • 计算属性(如树的高度、节点总数)。

*迭代法*

概念

  • 迭代(Iteration)是通过显式地使用数据结构(如栈或队列)来模拟递归过程。迭代法通过循环结构逐步解决问题。

特点

  • 需要显式管理数据结构(如栈或队列)。
  • 通常需要更多的代码和更复杂的逻辑来实现。

优点

  • 没有栈溢出风险:适用于非常深的树。
  • 更高效:在一些场景中,迭代法可能比递归法更高效。

缺点

  • 代码复杂:迭代代码通常比递归代码更复杂,难以理解和维护。

应用

  • 深度优先搜索(DFS):通常使用栈实现,适用于前序遍历、中序遍历和后序遍历。
  • 广度优先搜索(BFS):通常使用队列实现,适用于层序遍历。

栈和队列

  • 栈(Stack):使用深度搜索,适用于前序、中序和后序遍历。
  • 队列(Queue):使用广度搜索,适用于层序遍历。按层次从左到右。

总结
递归法适用于代码简洁和树结构自然表达的场景,但要注意栈深度和性能开销。迭代法适用于避免栈溢出和需要更高效的场景,尽管代码复杂度较高。通过对递归法和迭代法的总结,可以更好地理解和选择合适的方法来解决二叉树相关的问题。

*深度优先的三种遍历顺序*:

前序遍历 (Preorder Traversal)
1. 访问根节点。
2. 遍历左子树。
3. 遍历右子树

后序遍历 (Postorder Traversal)
1. 遍历左子树。
2. 遍历右子树。
3. 访问根节点。

中序遍历 (Inorder Traversal)
1. 遍历左子树。
2. 访问根节点。
3. 遍历右子树。


📝144.二叉树的前序遍历 (Easy)

📸解题代码:(前序遍历-深度搜索-递归法)


def preorderTraversal(root):
    def helper(node):
        if not node:
            return []
        result = [node.val]  # 处理当前节点
        result += helper(node.left)   # 递归处理左子树
        result += helper(node.right)  # 递归处理右子树
        return result
    
    return helper(root)

📸解题代码:(前序遍历-深度搜索-迭代法)


  • 时间复杂度为 O(n),其中 n 是二叉树的节点数。这是因为我们在遍历过程中对每个节点都只访问一次。
  • 空间复杂度是 O(n)。这是因为在最坏的情况下,堆栈可以容纳树的所有节点。在二叉树倾斜且所有节点都在一侧的最坏情况。
# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
#preorder - iretation(迭代法) - 深度搜索
class Solution(object):
    def preorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if not root: #如果是empty tree, root=none
            return []
        stack = [root] #stack用于暂时存放实时的node变化
        result = [ ] #result用于存放固定的node

        while stack: #make sure tree is not empty
            node = stack.pop() #将stack中pop出来一个数值,与此同时赋值给node
            result.append(node.val) #上一步pop出来的node永久存放在result中
            if node.right: #检查当前node右孩子是否为空,如果不是,append到stack里面
                stack.append(node.right)
            if node.left:  #检查当前node左孩子是否为空,如果不是,append到stack里面
                stack.append(node.left)
        return result

📝94.二叉树的中序遍历 (Easy)

📸解题代码:(中序遍历-深度搜索-递归法)


def inorderTraversal(root):
    def helper(node):
        if not node:
            return []
        result = []               # 创建结果列表
        result += helper(node.left)   # 递归处理左子树
        result.append(node.val)       # 处理当前节点
        result += helper(node.right)  # 递归处理右子树
        return result
    
    return helper(root)

📸解题代码:(中序遍历-深度搜索-迭代法)


# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution(object):
    def inorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if root is None:
            return []
        stack = []
        result = []
        current = root
        
        while stack or current:
            #外层 while 循环检查 stack 或 current 是否非空,决定是否继续遍历  
            while current: #内层while遍历当前节点及其所有左子节点,并全部压入stack
                stack.append(current)
                current = current.left
                #当已经将最左侧的孩子push进stack时,开始pop
            current = stack.pop()
            result.append(current.val)
            current = current.right #如果当前指针没有右孩子,则返回上上步骤,继续pop,直到把中间的node pop出来,这时候就可以寻找中间node右边的node了
        return result

📝115.二叉树的后序遍历 (Easy)

📸解题代码:(后序遍历-深度搜索-递归法)


def postorderTraversal(root):
    def helper(node):
        if not node:
            return []
        result = []               # 创建结果列表
        result += helper(node.left)   # 递归处理左子树
        result += helper(node.right)  # 递归处理右子树
        result.append(node.val)       # 处理当前节点
        return result
    
    return helper(root)

📸解题代码:(后序遍历-深度搜索-迭代法)


# Definition for a binary tree node.
class TreeNode(object):
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class Solution(object):
    def postorderTraversal(self, root):
        """
        :type root: TreeNode
        :rtype: List[int]
        """
        if root is None:
            return []
        
        stack = [root]
        result = []

        while stack:
            node = stack.pop()
            result.append(node.val)
            if node.left:
                stack.append(node.left)
            if node.right:
                stack.append(node.right)
        # Reverse the result list to get the correct postorder traversal
        return result[::-1]

☎️要点总结:

-迭代法:

通过观察可发现,前序遍历和后续遍历的模式是相似的,都是提前在stack中放入root;后续遍历这里我用了一个小技巧,前序遍历的顺序是先放入root-再放入右-最后放入左,这样pop出来的顺序就是“中左右”。后续遍历可以延续前序遍历的写法,只需要把左右放入stack的顺序调换,然后得到“中右左”,小技巧就是在最后一步调换顺序,变成了“左右中”。

但是,当我们写中序遍历时,代码就变得很不一样。首先,没有提前在stack中放入root,而是用了current指针,先指向root,又指向左,最后指向右。其中还夹杂了两个while循环。

为什么需要双层 `while` 循环:外层 `while` 循环:确保遍历在栈和当前节点均为空之前不会停止。它处理整个树的遍历过程;内层 `while` 循环:遍历所有左子节点并将其压入栈中,以便在访问根节点和右子树之前先处理左子树。这是因为中序遍历的特点是需要先处理左子树。

总的来说,前序遍历和后序遍历的模式非常相似,只是通过调整节点添加到栈的顺序来实现不同的遍历顺序。而中序遍历因为需要在访问根节点之前完全处理左子树(这是因为确保率先将左孩子加入result中),因此需要一个指针来跟踪当前节点,并使用双层 `while` 循环来确保左子树被完整遍历。

-递归法:

递归法的代码看上去十分简洁清晰,并且前中后三种遍历方法只需要反复调换三行代码的顺序即可,其余部分全都保持一致。但这个方法理解起来相对困难。我的建议是首先要彻底搞明白这是递归返回的基础,也就是base case。针对这题,base case就是当前遍历到的节点没有子节点时,停止遍历,返回到她的上一层节点,继续遍历另外一侧的节点树。

helper(root) 是一个递归函数,它从树的根节点 root 开始进行前序遍历。helper(root) 会按照前序遍历的顺序处理当前节点、左子树和右子树。helper(root) 的返回值被直接返回给 preorderTraversal 的调用者,这就是最后一行要return helper(root) 的原因。因此,return helper(root) 的实际作用是启动前序遍历,并将最终的遍历结果返回给调用 preorderTraversal 函数的代码。通过这种方式,我们可以得到整棵二叉树的前序遍历序列。

此外,当helper函数中放入没有子节点的node,会在return两次空列表后自动返回到本节点的上一层节点,这是为什么?举个例子,假设我们创建了如下的二叉树,执行前序遍历:

当我们递归处理节点 4 时,因为它没有子节点,所以递归调用 helper(None) 返回空列表,然后返回上一层的 2。这样做是为了确保递归过程能够正确地遍历整棵二叉树。让我们详细解释为什么会这样做:

  • 递归处理节点 4: 当递归处理节点 4 时,执行 helper(node) 函数,其中 node 是 4。在 helper(4) 函数内部,首先处理当前节点 4,将其值 4 添加到结果列表 result。然后递归调用 helper(4.left) 处理左子树。因为 4 没有左子节点(即 4.left 是 None),所以 helper(4.left) 实际上是 helper(None)。
  • 处理空节点:在 helper(None) 中,因为 node 是 None,满足 if not node 条件,返回一个空列表 []。递归调用 helper(4.right) 处理右子树。同样,因为 4 没有右子节点(即 4.right 是 None),所以 helper(4.right) 实际上是 helper(None)。
  • 返回上一层:helper(None) 再次返回空列表 [],所以 helper(4) 最终返回 [4],即包含当前节点 4 的前序遍历结果。现在我们回到处理节点 2 继续处理helper(2.right)。在这之前,针对节点2,我们已经彻底处理完了 ‘helper(2.left)' (在处理完左子树(节点 4)之后,将返回的结果 [4] 添加到 result 列表中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值