[Leetcode] [Tutorial] 二叉树


# 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

226. 翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

Solution

class Solution:
    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return None

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

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

        return root

108. 将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

示例:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]

解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

Solution

class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        if not nums:
            return None
        
        mid = len(nums) // 2
        node = TreeNode(nums[mid])
        node.left = self.sortedArrayToBST(nums[:mid])
        node.right = self.sortedArrayToBST(nums[mid + 1:])

        return node

101. 对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

Solution

一个二叉树是对称的当且仅当它的左子树和右子树是镜像的。两个树是镜像的当且仅当:

  • 两个树的根节点具有相同的值。
  • 第一个树的左子树和第二个树的右子树是镜像的。
  • 第一个树的右子树和第二个树的左子树是镜像的。
class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        return self.isMirror(root, root)
    
    def isMirror(self, left: Optional[TreeNode], right: Optional[TreeNode]) -> bool:
        if left is None and right is None:
            return True
        if left is None or right is None:
            return False
        return (left.val == right.val) and self.isMirror(left.right, right.left) and self.isMirror(left.left, right.right)

这个问题还可以借助层次遍历来解决。

class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True

        queue = deque([root])

        while queue:
            level = []
            for _ in range(len(queue)):
                node = queue.popleft()
                if node:
                    level.append(node.val)
                    queue.append(node.left)
                    queue.append(node.right)
                else:
                    level.append(None)

            if level != level[::-1]:
                return False
        
        return True

需要注意的是,应当将 None(空)子节点也视为有效节点,并在 level 列表中为其保留位置。

为了进一步优化这个算法的实现,我们可以尝试在每一层进行对称性检查的时候就立即停止,而不是总是等到整个层次遍历完成。也就是说,我们可以在将一个节点的左右子节点加入队列的同时,就检查他们是否对称。如果他们不对称,那么我们就知道整个树不对称,所以可以立即返回 False。

class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True

        queue = deque([(root.left, root.right)])

        while queue:
            node1, node2 = queue.popleft()
            if not node1 and not node2:
                continue
            if not node1 or not node2 or node1.val != node2.val:
                return False
            queue.append((node1.left, node2.right))
            queue.append((node1.right, node2.left))

        return True

102. 二叉树的层序遍历

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

示例:

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

Solution

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

        queue = deque([(root, 0)]) # 添加一个索引表示层级
        res = []

        while queue:
            node, level = queue.popleft()

            # 如果结果列表的长度小于当前层级加1,说明这是新的一层,我们需要添加一个新的列表
            if len(res) == level:
                res.append([])

            res[level].append(node.val)

            if node.left:
                queue.append((node.left, level+1))
            if node.right:
                queue.append((node.right, level+1))

        return res

199. 二叉树的右视图

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

示例:

输入: [1,2,3,null,5,null,4]
输出: [1,3,4]

Solution

我们可以对二叉树进行层次遍历,那么对于每层来说,最右边的结点一定是最后被遍历到的。

class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        queue = deque([root])
        rightside = []
        while queue:
            level_length = len(queue)
            for i in range(level_length):
                node = queue.popleft()
                if i == level_length - 1:
                    rightside.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return rightside

深度优先搜索也是一种解决这个问题的方法。我们可以用一个变量depth来记录当前的深度。我们优先访问右子树,然后是左子树。在访问每一个节点的时候,如果当前的深度大于我们之前记录的最大深度,就说明这个节点是我们从右边能看到的节点,我们就将这个节点加入到结果列表中。

class Solution:
    def rightSideView(self, root: TreeNode) -> List[int]:
        rightmost_value_at_depth = dict() # 深度为索引,存放节点的值
        max_depth = -1

        stack = [(root, 0)]
        while stack:
            node, depth = stack.pop()

            if node is not None:
                # 维护二叉树的最大深度
                max_depth = max(max_depth, depth)

                # 如果不存在对应深度的节点我们才插入
                rightmost_value_at_depth.setdefault(depth, node.val)

                stack.append((node.left, depth + 1))
                stack.append((node.right, depth + 1))

        return [rightmost_value_at_depth[depth] for depth in range(max_depth + 1)]

104. 二叉树的最大深度

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

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

Solution

这个问题可以通过使用深度优先搜索来解决。

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root is None: 
            return 0 
        else: 
            left_height = self.maxDepth(root.left)
            right_height = self.maxDepth(root.right)
            return max(left_height, right_height) + 1

广度优先搜索也可以解决这个问题。

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        queue = deque([root])
        depth = 0
        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

需要注意的是,queue.pop()会从队列的右侧取出节点。

543. 二叉树的直径

给你一棵二叉树的根节点,返回该树的 直径 。

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。

两节点之间路径的 长度 由它们之间边数表示。

示例:

输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。

Solution

解决这个问题的关键在于理解二叉树的直径不一定会经过根节点。直径可以是任意两个叶子节点之间的最长路径。这条路径可以看作是某个节点的左子树的深度加上右子树的深度。

因此,可以使用深度优先搜索的方式遍历每个节点,并且在遍历的过程中记录下最大的左子树深度加右子树深度的和,这个和就是我们要求的直径。

class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        self.res = 0
        def depth(node):
            if not node: 
                return 0
            left, right = depth(node.left), depth(node.right)
            self.res = max(self.res, left + right)
            return max(left, right) + 1

        depth(root)
        return self.res

437. 路径总和 III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3

Solution

为了解决这个问题,我们可以使用一个类似于双重递归的方法。第一层递归遍历每一个节点,对于每一个节点我们都当作它是路径的起点再做一次递归。

此外,我们可以使用一个前缀和的方法来优化我们的算法。前缀和是一个在递归过程中维持的累积值,它表示从根到当前节点的路径上的节点值之和。当我们计算了前缀和后,我们只需要检查前缀和与targetSum的差值有多少次出现在之前的路径中,这就是当前路径数量。

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        self.res = 0
        prefix = {
    
    0:1}
        
        # 辅助函数计算前缀和
        def dfs(node, curr_sum):
            if not node:
                return
            
            curr_sum += node.val
            
            # 当前前缀和减去targetSum如果存在,则表示找到了一个路径
            self.res += prefix.get(curr_sum - targetSum, 0)
            prefix[curr_sum] = prefix.get(curr_sum, 0) + 1
            
            dfs(node.left, curr_sum)
            dfs(node.right, curr_sum)
            
            # 回溯,去除当前节点的前缀和
            prefix[curr_sum] -= 1
        
        dfs(root, 0)
        return self.res

105. 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

Solution

  1. 从前序遍历中取出第一个元素,这个元素是当前的根节点。
  2. 在中序遍历中找到这个根节点的位置。设为中点。
  3. 中序遍历中中点左边的序列即为当前根节点的左子树的中序遍历,中点右边的序列即为当前根节点的右子树的中序遍历。
  4. 对于前序遍历,除去第一个根节点后,接下来的前len(左子树中序遍历)的部分即为左子树的前序遍历,其余部分即为右子树的前序遍历。
  5. 递归构造左右子树。
class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        if not preorder:
            return None

        root = TreeNode(preorder[0])
        mid_idx = inorder.index(preorder[0])

        root.left = self.buildTree(preorder[1:mid_idx+1], inorder[:mid_idx])
        root.right = self.buildTree(preorder[mid_idx+1:], inorder[mid_idx+1:])
        return root

注意,在上述实现中,我们每次递归调用都产生了新的列表切片,这实际上增加了额外的时间和空间开销。为了提高效率,可以在函数参数中传入子数组的边界,避免产生新的切片。

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        def buildTreeFromPreInOrders(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int):
            if preorder_left > preorder_right:
                return None

            preorder_root = preorder_left
            inorder_root = index[preorder[preorder_root]]

            root = TreeNode(preorder[preorder_root])
            size_left_subtree = inorder_root - inorder_left
            root.left = buildTreeFromPreInOrders(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1)
            root.right = buildTreeFromPreInOrders(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right)
            return root

        n = len(preorder)
        index = {
    
    element: i for i, element in enumerate(inorder)}
        return buildTreeFromPreInOrders(0, n - 1, 0, n - 1)

236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。

示例:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5

Solution

  1. 如果当前节点的值与p或q的值匹配,那么这意味着我们找到了p或q的一个节点。
  2. 如果在左子树和右子树中都发现了p或q,则当前节点就是他们的最近公共祖先。
  3. 如果左子树中发现了p或q,而右子树没有,则这意味着他们的最近公共祖先位于左子树。
  4. 同理,如果右子树中发现了p或q,而左子树没有,则他们的最近公共祖先位于右子树。
class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if not root:
            return None
        if root == p or root == q:
            return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if left and right:
            return root
        return left if left else right

114. 二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例:

输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]

Solution

解决这个问题的核心思路是:先将左子树插入到右子树的地方,然后将原来的右子树接到现在右子树的最右边节点。这样就可以确保树在展平后,所有的节点都按照先序遍历的顺序连接在一起。

class Solution:
    def flatten(self, root: Optional[TreeNode]) -> None:
        node = root
        while node:
            # 如果当前节点的左孩子不为空,则找到当前节点左子树的最右节点
            if node.left:
                temp = node.left
                while temp.right:
                    temp = temp.right

                # 让当前节点左子树的最右节点的右指针指向当前节点的右子树
                temp.right = node.right
                # 将当前节点的左子树插入到当前节点和当前节点右子树之间
                node.right = node.left
                node.left = None

            # 继续处理链表中的下一个节点
            node = node.right

我们之后可以知道,在标准的 Morris 遍历中,我们会先从根节点开始,寻找它的前驱节点,并且在前驱节点和当前节点之间建立链接。然后再遍历它的右子树。在这个问题中,我们只需要稍作修改,就可以把二叉树展开为单链表。

144. 二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

Solution

如果我们采用递归的方式来实现这个算法,那么代码将非常直观和简单。

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

需要注意的是,当递归调用 inorder(node.left) 和 inorder(node.right) 返回的时候,它们返回的都是一个列表,如果将这些列表直接 append 到 res 中,就会导致 res 实际上是一个嵌套列表(列表中的元素是列表),并非我们想要的扁平化的结果列表。

解决方法是使用列表的 extend 方法,而不是 append。extend 可以将一个列表中的每个元素添加到另一个列表中,而 append 会将整个列表作为一个元素添加。

我们也可以用迭代的方式来实现,区别在于递归的时候隐式地维护了一个栈。

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

        while stack:
            root = stack.pop()
            if root is not None:
                res.append(root.val)
                if root.right is not None:
                    stack.append(root.right)
                if root.left is not None:
                    stack.append(root.left)
        
        return res

145. 二叉树的后序遍历

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。

Solution

前序遍历是根 -> 左 -> 右,后序遍历是左 -> 右 -> 根。可以稍微修改前序遍历,使之变为根 -> 右 -> 左。

然后将结果逆序,就可以得到后序遍历。

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        
        stack = [root]
        res = []
        
        while stack:
            root = stack.pop()
            if root is not None:
                res.append(root.val)
                if root.left is not None:  # 注意这里和前序遍历相反,先压入左节点
                    stack.append(root.left)
                if root.right is not None:  # 后压入右节点
                    stack.append(root.right)
        
        return res[::-1]  # 注意这里要将结果逆序

94. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

Solution

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

颜色标记法是一种通用且简洁的树遍历方法,它可以用于任何顺序的树的深度优先遍历。其主要思想是使用颜色标记节点的状态,通常使用两种颜色:

白色表示节点已经被创建,但还没有被处理,即还没有被遍历过;灰色表示节点已经被遍历过。

我们使用一个栈来存储节点和其颜色。如果碰到的节点为白色,则将其标记为灰色,然后将其孩子节点按照遍历的顺序(前序、中序、后序)入栈。如果碰到的节点为灰色,则将节点的值输出。

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        WHITE, GRAY = 0, 1
        res = []
        stack = [(WHITE, root)]
        while stack:
            color, node = stack.pop()
            if node is None: continue
            if color == WHITE:
                stack.append((WHITE, node.right))
                stack.append((GRAY, node))
                stack.append((WHITE, node.left))
            else:
                res.append(node.val)
        return res

Morris 遍历是一种遍历二叉树的算法,其特点是在不使用额外空间(比如栈或者递归调用栈)的情况下实现对二叉树的遍历。Morris 遍历的核心思想是利用二叉树中的空闲指针(这些指针在树中的原始形状中并没有被使用),作为在遍历过程中找到某个节点的前驱和后继节点的线索。

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

230. 二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

Solution

class Solution:
    def kthSmallest(self, root: TreeNode, k: int) -> int:
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            k -= 1
            if k == 0:
                return root.val
            root = root.right

98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

Solution

要解决这道题首先我们要了解二叉搜索树有什么性质可以给我们利用,由题目给出的信息我们可以知道:如果该二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左右子树也为二叉搜索树。

这启示我们设计一个递归函数 inorder(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r) 的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        def inorder(node, lower=float('-inf'), upper=float('inf')):
            if not node:
                return True

            val = node.val
            if val <= lower or val >= upper:
                return False

            if not inorder(node.left, lower, val):
                return False
            if not inorder(node.right, val, upper):
                return False

            return True

        return inorder(root)

我们可以进一步知道二叉搜索树「中序遍历」得到的值构成的序列一定是升序的,这启示我们在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        stack = []
        prev = None
        curr = root
        
        while stack or curr:
            while curr:
                stack.append(curr)
                curr = curr.left
            curr = stack.pop()
            
            if prev is not None and curr.val <= prev:
                return False
            
            prev = curr.val
            curr = curr.right
            
        return True

猜你喜欢

转载自blog.csdn.net/weixin_45427144/article/details/131534960