[Leetcode] [Tutorial] Binary Tree


# 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. Flip a binary tree

Given the root node root of a binary tree, flip the binary tree and return its root node.

Example:

Input: root = [4,2,7,1,3,6,9]
Output: [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. Convert ordered array to binary search tree

You are given an integer array nums, in which the elements have been sorted in ascending order. Please convert it into a height-balanced binary search tree.

A height-balanced binary tree is a binary tree that satisfies "the absolute value of the height difference between the left and right subtrees of each node does not exceed 1".

Example:
Input: nums = [-10,-3,0,5,9]
Output: [0,-3,9,-10,null,5]

Explanation: [0,-10,5,null,-3,null,9] will also be considered the correct answer:

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. Symmetric binary tree

Given the root node root of a binary tree, check whether it is axially symmetrical.

Solution

A binary tree is symmetric if and only if its left and right subtrees are mirror images. Two trees are mirror images if and only if:

  • The root nodes of both trees have the same value.
  • The left subtree of the first tree and the right subtree of the second tree are mirrored.
  • The right subtree of the first tree and the left subtree of the second tree are mirrored.
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)

This problem can also be solved with the help of level traversal.

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

It should be noted that None (empty) child nodes should also be regarded as valid nodes and reserve a place for them in the level list.

In order to further optimize the implementation of this algorithm, we can try to stop immediately when the symmetry check is performed at each layer, instead of always waiting until the entire layer traversal is completed. In other words, we can check whether the left and right child nodes of a node are symmetrical while adding them to the queue. If they are not symmetrical, then we know that the entire tree is not symmetrical, so we can return False immediately.

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. Level-order traversal of binary tree

Given the root node root of a binary tree, return the level-order traversal of its node values. (i.e. visit all nodes layer by layer, from left to right).

Example:

Input: root = [3,9,20,null,null,15,7]
Output: [[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. Right view of binary tree

Given the root node root of a binary tree, imagine yourself standing on the right side of it, and return the node values ​​that can be seen from the right side in order from top to bottom.

Example:

Input: [1,2,3,null,5,null,4]
Output: [1,3,4]

Solution

We can perform hierarchical traversal of a binary tree, so for each level, the rightmost node must be the last one traversed.

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-first search is also a way to solve this problem. We can use a variable depth to record the current depth. We access the right subtree first, then the left subtree. When visiting each node, if the current depth is greater than the maximum depth we recorded before, it means that this node is a node we can see from the right, and we will add this node to the result list.

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. Maximum depth of binary tree

Given a binary tree, find its maximum depth.

The depth of a binary tree is the number of nodes on the longest path from the root node to the farthest leaf node.

Solution

This problem can be solved by using depth-first search.

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

Breadth-first search can also solve this problem.

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

It should be noted that queue.pop() will remove nodes from the right side of the queue.

543. Diameter of binary tree

Given the root node of a binary tree, return the diameter of the tree.

The diameter of a binary tree is the length of the longest path between any two nodes in the tree. This path may or may not pass through the root node root.

The length of the path between two nodes is represented by the number of edges between them.

Example:

Input: root = [1,2,3,4,5]
Output: 3
Explanation: 3, take the length of the path [4,2,1,3] or [5,2,1,3].

Solution

The key to solving this problem is to understand that the diameter of a binary tree does not necessarily pass through the root node. The diameter can be the longest path between any two leaf nodes. This path can be viewed as the depth of the left subtree of a node plus the depth of the right subtree.

Therefore, you can use depth-first search to traverse each node, and record the sum of the maximum left subtree depth plus the right subtree depth during the traversal process. This sum is the diameter we require.

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. Path Sum III

Given the root node root of a binary tree and an integer targetSum, find the number of paths in the binary tree whose sum of node values ​​is equal to targetSum.

The path does not need to start from the root node, nor does it need to end at a leaf node, but the path direction must be downward (only from parent node to child node).

Example:

Input: root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
Output: 3

Solution

To solve this problem, we can use an approach similar to double recursion. The first level of recursion traverses each node, and for each node we treat it as the starting point of the path and do another recursion.

Additionally, we can use a prefix sum approach to optimize our algorithm. The prefix sum is a cumulative value maintained during recursion that represents the sum of node values ​​along the path from the root to the current node. After we calculate the prefix sum, we only need to check how many times the difference between the prefix sum and targetSum appears in the previous path, which is the current path number.

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. Construct a binary tree from preorder and inorder traversal sequences

Given two integer arrays preorder and inorder, where preorder is a preorder traversal of a binary tree and inorder is an inorder traversal of the same tree, please construct a binary tree and return its root node.

Solution

  1. Remove the first element from the preorder traversal, which is the current root node.
  2. Find the location of this root node in inorder traversal. Set as midpoint.
  3. In in-order traversal, the sequence to the left of the midpoint is an in-order traversal of the left subtree of the current root node, and the sequence to the right of the midpoint is an in-order traversal of the right subtree of the current root node.
  4. For preorder traversal, after removing the first root node, the next part of the first len ​​(mid-order traversal of the left subtree) is the preorder traversal of the left subtree, and the remaining part is the preorder traversal of the right subtree.
  5. Construct left and right subtrees recursively.
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

Note that in the above implementation, we generate a new list slice for each recursive call, which actually adds additional time and space overhead. To improve efficiency, the bounds of the subarray can be passed in the function parameters to avoid generating new slices.

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. Recent common ancestor of binary tree

Given a binary tree, find the nearest common ancestor of two specified nodes in the tree.

For two nodes p and q of a rooted tree T, the nearest common ancestor is represented as a node x, such that x is the ancestor of p and q and the depth of x is as large as possible (a node can also be its own ancestor).

Example:

Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
Output: 5

Solution

  1. If the value of the current node matches the value of p or q, then it means we found a node for p or q.
  2. If p or q is found in both the left and right subtrees, the current node is their nearest common ancestor.
  3. If p or q is found in the left subtree but not in the right subtree, it means that their most recent common ancestor is located in the left subtree.
  4. In the same way, if p or q is found in the right subtree but not in the left subtree, their most recent common ancestor is located in the right subtree.
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. Expand binary tree into linked list

Given the root node root of the binary tree, please expand it into a singly linked list:

  • The expanded singly linked list should also use TreeNode, where the right sub-pointer points to the next node in the linked list, and the left sub-pointer is always null.
  • The expanded singly linked list should be in the same order as the binary tree preorder traversal.

Example:

Input: root = [1,2,5,3,4,null,6]
Output: [1,null,2,null,3,null,4,null,5,null,6]

Solution

The core idea to solve this problem is: first insert the left subtree into the right subtree, and then connect the original right subtree to the rightmost node of the current right subtree. This ensures that after the tree is flattened, all nodes are connected in the order of pre-order traversal.

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

We can know later that in standard Morris traversal, we will start from the root node, find its predecessor nodes, and establish a link between the predecessor node and the current node. Then traverse its right subtree. In this problem, we only need to make slight modifications to expand the binary tree into a singly linked list.

144. Preorder traversal of binary tree

Given the root node root of a binary tree, return the preorder traversal of its node values.

Solution

If we implement this algorithm recursively, the code will be very intuitive and simple.

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

It should be noted that when recursive calls inorder(node.left) and inorder(node.right) return, they return a list. If these lists are directly appended to res, res will actually be A nested list (the elements in the list are lists) is not the flat result list we want.

The solution is to use the extend method of the list instead of append. extend adds each element in one list to another list, while append adds the entire list as a single element.

We can also implement it iteratively. The difference is that a stack is implicitly maintained during recursion.

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. Post-order traversal of binary tree

Given the root node root of a binary tree, return the post-order traversal of its node values.

Solution

Preorder traversal is root->left->right, and postorder traversal is left->right->root. You can slightly modify the preorder traversal so that it becomes root -> right -> left.

Then reverse the results to get post-order traversal.

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. In-order traversal of binary tree

Given the root node root of a binary tree, return its inorder traversal.

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

Color notation is a general and concise tree traversal method that can be used for depth-first traversal of trees in any order. The main idea is to use colors to mark the status of nodes, usually using two colors:

White indicates that the node has been created but has not been processed, that is, it has not been traversed; gray indicates that the node has been traversed.

We use a stack to store nodes and their colors. If the node encountered is white, mark it as gray, and then push its child nodes onto the stack in the order of traversal (pre-order, mid-order, post-order). If the node encountered is gray, the value of the node is output.

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 traversal is an algorithm for traversing a binary tree, which is characterized by traversing the binary tree without using additional space (such as a stack or a recursive call stack). The core idea of ​​Morris traversal is to use free pointers in the binary tree (these pointers are not used in the original shape of the tree) as clues to find the predecessor and successor nodes of a node during the traversal process.

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. Kth smallest element in binary search tree

Given the root node root of a binary search tree, and an integer k, please design an algorithm to find the k-th smallest element (counting from 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. Verify binary search tree

Given the root node root of a binary tree, determine whether it is a valid binary search tree.

A valid binary search tree is defined as follows:

  • The left subtree of a node only contains numbers less than the current node.
  • The right subtree of a node only contains numbers greater than the current node.
  • All left and right subtrees must themselves be binary search trees.

Solution

To solve this problem, we first need to understand what properties of the binary search tree can be used by us. From the information given in the question, we can know: If the left subtree of the binary tree is not empty, then the values ​​of all nodes on the left subtree The values ​​are all less than the value of its root node; if its right subtree is not empty, the values ​​of all nodes on the right subtree are greater than the value of its root node; its left and right subtrees are also binary search trees.

This inspires us to design a recursive function inorder(root, lower, upper) for recursive judgment. The function means considering the subtree with root as the root and judging whether the values ​​of all nodes in the subtree are within the range of (l, r) ( Note that it is an open interval). If the value val of the root node is not within the range of (l, r), it means that the condition is not met and returns directly. Otherwise, we have to continue the recursive call to check whether its left and right subtrees are satisfied. If they are all satisfied, it means that this is a binary search tree. .

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)

We can further know that the sequence of values ​​obtained by "in-order traversal" of the binary search tree must be in ascending order. This enlightens us to check in real time whether the value of the current node is greater than the value of the previous node traversed during in-order traversal. Just value.

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

Guess you like

Origin blog.csdn.net/weixin_45427144/article/details/131534960