二叉树的递归和迭代遍历、层序遍历

156 阅读15分钟

递归算法的三个要素

  1. 确定递归函数的参数和返回值:  确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件:  写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑:  确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

以下以前序遍历为例:

  1. 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入列表来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是None,代码如下:
def traversal(node, vec)
  1. 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if node is None:
    return
  1. 确定单层递归的逻辑:前序遍历是中左右的顺序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
vec.append(node.val)    # 中
traversal(node.left, vec)  # 左
traversal(node.right, vec) # 右

前序递归

# 前序遍历-递归-LC144_二叉树的前序遍历
 # 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 dfs(self,node,ans):#depth-first search
        if node is None:
            return
        
        ans.append(node.val)
        self.dfs(node.left,ans)
        self.dfs(node.right,ans)

    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans=[]
        self.dfs(root,ans)
        return ans

#1.确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

#2.确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

#3.确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

中序递归

# 中序遍历-递归-LC94_二叉树的中序遍历
# 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 dfs(self,node,ans):
        if node is None:
            return 
        
        
        self.dfs(node.left,ans)
        ans.append(node.val)
        self.dfs(node.right,ans)
        
    
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans=[]
        self.dfs(root,ans)
        return ans        

后序递归

# 后序遍历-递归-LC145_二叉树的后序遍历
# 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 dfs(self,node,ans):#depth-first search
        if node is None:
            return

        self.dfs(node.left,ans)
        self.dfs(node.right,ans)
        ans.append(node.val)
        
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans=[]
        self.dfs(root,ans)
        return ans

        

迭代遍历

为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢?

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

此时应该知道我们用栈也可以是实现二叉树的前后中序遍历了。

前序遍历(迭代法)

我们先看一下前序遍历。

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。

动画如下:

%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif

# 前序遍历-迭代-LC144_二叉树的前序遍历
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        # 根结点为空则返回空列表
        if not root:
            return []
        stack = [root]
        result = []
        while stack:
            node = stack.pop()
            # 中结点先处理
            result.append(node.val)
            # 右孩子先入栈
            if node.right:
                stack.append(node.right)
            # 左孩子后入栈
            if node.left:
                stack.append(node.left)
        return result

中序遍历(迭代法)

在迭代的过程中,其实我们有两个操作:

  1. 处理:将元素放进result数组中
  2. 访问:遍历节点

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

动画如下:

%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif

中序遍历,可以写出如下代码:

# 中序遍历-迭代-LC94_二叉树的中序遍历
class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        if not root:
            return []
        stack = []  # 不能提前将root结点加入stack中
        result = []
        cur = root
        while cur or stack:
            # 先迭代访问最底层的左子树结点
            if cur:     
                stack.append(cur)
                cur = cur.left		
            # 到达最左结点后处理栈顶结点    
            else:		
                cur = stack.pop()
                result.append(cur.val)
                # 取栈顶元素右结点
                cur = cur.right	
        return result

后序遍历(迭代法)

再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

image.png

所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:

# 后序遍历-迭代-LC145_二叉树的后序遍历
class Solution:
   def postorderTraversal(self, root: TreeNode) -> List[int]:
       if not root:
           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)
       # 将最终的数组翻转
       return result[::-1]

层序遍历

102.二叉树的层序遍历

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

image.png

层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。

需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。

而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。

使用队列实现二叉树广度优先遍历,动画如下:

102%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.gif

这样就实现了层序从左到右遍历二叉树。

# 利用长度法
# 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 levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
            
        queue = collections.deque()
        queue.append(root)
        result = []
        
        while queue:
            level = []
            for _ in range(len(queue)):
                cur = queue.popleft()
                level.append(cur.val)
                
                if cur.left:
                    queue.append(cur.left)
                    
                if cur.right:
                    queue.append(cur.right)
                    
            result.append(level)
            
        return result

199.二叉树的右视图

给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

image.png

层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。

# 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 rightSideView(self, root: TreeNode) -> List[int]:
        if not root:
            return []
        
        queue = collections.deque([root])
        right_view = []
        
        while queue:
            level_size = len(queue)
            
            for i in range(level_size):
                node = queue.popleft()
                
                if i == level_size - 1:
                    right_view.append(node.val)
                
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        
        return right_view

637.二叉树的层平均值

给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。

image.png 本题就是层序遍历的时候把一层求个总和再取一个均值。

   """二叉树层平均值迭代解法"""

# 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 averageOfLevels(self, root: TreeNode) -> List[float]:
        if not root:
            return []

        queue = collections.deque([root])
        averages = []
        
        while queue:
            size = len(queue)
            level_sum = 0
            
            for i in range(size):
                node = queue.popleft()
                
                
                level_sum += node.val
                    
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            
            averages.append(level_sum / size)
        
        return averages

429.N叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。

例如,给定一个 3叉树 :

image.png

返回其层序遍历:

[ [1], [3,2,4], [5,6] ] 这道题依旧是模板题,只不过一个节点有多个孩子了

"""
# Definition for a Node.
class Node:
    def __init__(self, val: Optional[int] = None, children: Optional[List['Node']] = None):
        self.val = val
        self.children = children
"""

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

        ans=[]
        queue=collections.deque()
        queue.append(root)

        while queue:
            temp=[]

            len_size=len(queue)

            for i in range(len_size):
                node=queue.popleft()
                temp.append(node.val)

                for child in node.children:
                    queue.append(child)
                
            ans.append(temp)

        return ans

515.在每个树行中找最大值

在二叉树的每一行中找到最大的值。

image.png

层序遍历,取每一层的最大值

# 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 largestValues(self, root: TreeNode) -> List[int]:
        if not root:
            return []

        result = []
        queue = collections.deque([root])

        while queue:
            level_size = len(queue)
            max_val = float('-inf')

            for _ in range(level_size):
                node = queue.popleft()
                max_val = max(max_val, node.val)

                if node.left:
                    queue.append(node.left)

                if node.right:
                    queue.append(node.right)

            result.append(max_val)

        return result

116.填充每个节点的下一个右侧节点指针

给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。

初始状态下,所有 next 指针都被设置为 NULL。

image.png 本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了

"""
# Definition for a Node.
class Node:
    def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
        self.val = val
        self.left = left
        self.right = right
        self.next = next
"""
class Solution:
    def connect(self, root: 'Node') -> 'Node':
        if not root:
            return root
        
        queue = collections.deque([root])
        
        while queue:
            level_size = len(queue)
            prev = None
            
            for i in range(level_size):
                node = queue.popleft()
                
                if prev:
                    prev.next = node
                
                prev = node
                
                if node.left:
                    queue.append(node.left)
                
                if node.right:
                    queue.append(node.right)
        
        return root

104.二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7],返回它的最大深度 3

image.png

使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。

在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示:

image.png

所以这道题的迭代法就是一道模板题,可以使用二叉树层序遍历的模板来解决的。

迭代法

# 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 maxDepth(self, root: TreeNode) -> int:
        if not root:
            return 0
        
        depth = 0
        queue = collections.deque([root])
        
        while queue:
            depth += 1
            for _ in range(len(queue)):
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        
        return depth

递归法

image.png

自底向上,从叶子节点开始,一层一层的向上,最终汇集在根节点。

这种求的其实是二叉树的高度。先遍历左子树,找出最大高度,再遍历右子树找出最大高度,最后在根节点取左子树和右子树高度值大的那个,加上根节点的高度 1,即 max(leftHeight, rightHeight) + 1 为当前二叉树的最大高度。

因为二叉树的最大高度 = 最大深度,所以即可求出二叉树的最大深度。

可以看出,这种先递归左子树、再递归右子树,最后再根节点,用的其实是后序遍历。

# 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 maxDepth(self, root: Optional[TreeNode]) -> int:
        return self.getHight(root)

#递归 后序遍历
    def getHight(self,root):
        if not root:
            return 0

        leftHight=self.getHight(root.left)

        rightHight=self.getHight(root.right)

        hight=1+max(leftHight,rightHight)

        return hight

111.二叉树的最小深度

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明: 叶子节点是指没有子节点的节点。


递归法

递归法,我以自底向上方式为例。

既然是用递归法,那还是按照往常,祭出递归二步曲:

(1) 找出重复的子问题。

后序遍历的顺序是:左子树、右子树、根。

在本题同样也是这个顺序:递归左子树的最小高度,递归右子树的最小高度,求根的最小高度。

对于左子树和右子树来说,也都是同样的操作。

#左子树最小值和右子树最小值
leftMindepth = self.minDepth(root.left) 
rightMindepth = self.minDepth(root.right) 

求二叉树的最小深度也就成了:

min(leftMindepth, rightMindepth) + 1

image.png 如果你到这就结束了,那很遗憾的告诉你,这道题你已经完蛋了...

我们再来看题,题目中定义“最小深度是从根节点到最近叶子节点的最短路径上的节点数量”。

我们都知道叶子节点是指没有左右孩子的节点。如果你只做到了上面那样的求解,那像下面这种二叉树:

image.png

你得到的结果会是:minDepth = min(leftMindepth, rightMindepth) + 1 = min(1, 0) + 1 = 1。

这个结果显然是错误的。这样的解法是把根节点 1 当成了叶子节点!而根节点 1 有左孩子,它不是叶子节点!

这就是这道题和之前求二叉树的最大深度不同的地方,也是这道题最大的坑!稍不注意,就完犊子!

因为再三强调:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 上面的二叉树只有左子树才有叶子节点,而右子树是空的,没有叶子节点,也就不存在从根节点到右子树的最小深度。

所以它的最小深度应该是 2。

所以,对于重复子问题应该分情况讨论:

第 1 种情况,只有根节点的,最小深度为 1。

#只有根节点,最小高度为 1
if root.left == None and root.right == None: 
    return 1

第 2 种情况,只有左子树或者只有右子树的,最小深度为“左子树的最小深度 + 1” 或者“右子树的最小深度 + 1”。

#如果节点的左子树不为空,右子树为空 
if root.left != None and root.right == None: 
    return leftMindepth + 1 
# 如果节点的右子树不为空,左子树为空 
if root.left == None and root.right != None: 
    return rightMindepth + 1 

第 3 种情况,左子树和右子树都有的,最小深度为“min(左子树的最小深度,右子树的最小深度) + 1”

# 左右子树都不为空 
return min(leftMindepth, rightMindepth) + 1

(2) 确定终止条件。

还是一样的,对于二叉树的遍历来说,想终止,即没东西遍历了,没东西遍历自然就停下来了。

那就是当前的节点是空的,既然是空的那就没啥好遍历。

# 二叉树为空,最小高度为 0 
if root == None: return 0

这两点确定好了,代码也就出来了

# 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 minDepth(self, root: Optional[TreeNode]) -> int:
        return self.getDepth(root)
    def getDepth(self,root):
        if not root:
            return 0

        leftDepth=self.getDepth(root.left)

        rightDepth=self.getDepth(root.right)

        if root.left!=None and root.right==None:
            return leftDepth+1

        if root.right!=None and root.left==None:
            return rightDepth+1
        
        depth=min(leftDepth,rightDepth)+1

        return depth

226.翻转二叉树

image.png

想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。

关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序)

遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。

注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果

这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了

递归法

我们下文以前序遍历为例,通过动画来看一下翻转的过程:

%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91.gif

前序遍历

# 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 invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return root

        root.left,root.right=root.right,root.left

        self.invertTree(root.left)
        self.invertTree(root.right)

        return root

后序遍历

# 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 invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return root

        self.invertTree(root.left)
        self.invertTree(root.right)
        
        root.left,root.right=root.right,root.left

中序遍历

# 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 invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return root

        self.invertTree(root.left)#左
        
        root.left,root.right=root.right,root.left#中
        
        self.invertTree(root.left)# 注意 这里依然要遍历左孩子,因为中间节点已经翻转了