Anthony's Blog Site

Cracking LeetCode 75 (Part 2)

February 28, 2025

Binary Tree

Photo Credit: ChatGPT 4o


This blog post aims to walk through classic coding problems listed in LeetCode 75.

Topics covered in this post are: Linked List, Binary Tree (DFS and BFS), and Binary Search Tree (BST).

All solutions are written in Python and are optimized for performance and readability.

Linked List

2095. Delete the Middle Node of a Linked List

class Solution:
    def deleteMiddle(self, head: Optional[ListNode]) -> Optional[ListNode]:

        # Intuition:
        # We use a pointer to go through the linked list while using another pointer
        # that keeps track of the middle node between the head and current node.

        # Edge case: if the list has only one node, return None
        if not head.next:
            return None
        
        slow, fast = head, head
        pre_slow = None  # the previous node before `slow``
        
        # Use two-pointer technique to find the middle
        while fast and fast.next:  # ensures `fast.next.next` is safe to perform
            pre_slow = slow
            slow = slow.next
            fast = fast.next.next
        
        # Delete middle node:
        pre_slow.next = slow.next
        # In Python, `slow.next = None` is optional.

        # NOTE: In C++, we will have to do:
        # prev->next = curr->next;
        # curr->next = nullptr;  // optional. prevents accessing deleted memory.
        # delete curr;  # required. prevents memory leaks.
        
        return head

328. Odd Even Linked List

class Solution:
    def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:

        # Intuition: As we go through the linked list, we modify the node such that
        # odd nodes and even nodes are linked separately.

        if not head:
            return None
        
        number = 1
        curr = head
        prev_odd, prev_even = None, None
        even_head = None
        while curr:

            # Link odd nodes and even nodes separately:
            if number % 2 == 1:
                if prev_odd:
                    prev_odd.next = curr
                prev_odd = curr
            else:
                # Store the 1st even node:
                if number == 2:
                    even_head = curr
                if prev_even:
                    prev_even.next = curr
                prev_even = curr

            # Update:
            curr = curr.next
            number += 1
        
        # Append even nodes after all odd nodes:
        prev_odd.next = even_head

        # NOTE: We should make the latest even node (if any) point to None,
        # or the autotester will stuck in a loop:
        if prev_even:
            prev_even.next = None

        return head

206. Reverse Linked List

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:

        # Intuition:
        # 1 -> 2 -> 3 -> 4
        # 1 <- 2 -> 3 -> 4

        # base case (turns out to be handled by the code below):
        # if not head:
        #     return None
        
        curr_node = head
        prev_node = None
        while curr_node:
            next_node = curr_node.next  # "store"
            curr_node.next = prev_node  # "reverse"
            # update:
            prev_node = curr_node
            curr_node = next_node

        return prev_node  # when we exit the while loop, curr_node will be None.

2130. Maximum Twin Sum of a Linked List

class Solution:
    def pairSum(self, head: Optional[ListNode]) -> int:
        
        # Intuition:
        # Since this is a linked list, not an array,
        # it is critical to identify all pairs of twins _as_ we traverse through the linked list.

        # Step 1: Find the middle node
        slow, fast = head, head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        # `fast` is now at None at the very end of the linked list;
        # `slow` is now at the first node of the second half of the linked list.
        
        # Step 2: Reverse the second half starting from the middle node
        prev, curr = None, slow
        while curr:
            temp = curr.next  # store
            curr.next = prev  # swap
            prev = curr  # step
            curr = temp  # step
        # `prev` now points to the last node.

        # Step 3: Find the max twin sum
        max_sum = 0
        first, second = head, prev
        while second:
            max_sum = max(first.val + second.val, max_sum)  # update
            first = first.next
            second = second.next
        # `second` stops moving at Node (n/2 + 1) since its `next` points to None.

        return max_sum

Binary Tree - DFS

Maximum Depth of Binary Tree

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:

        # Intuition:
        # We run DFS to compute the depth by recursively calling maxDepth() on left & rigth subtrees.

        # Base case (we've reached the end nodes):
        if not root:
            return 0

        return max(
            self.maxDepth(root.left), self.maxDepth(root.right)
        ) + 1

872. Leaf-Similar Trees

class Solution:
    def leafSimilar(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool:

        # Intuition: Use pre-order DFS to find the leaf nodes.

        def dfs(node, seq):
            if not node:
                return
            if not node.left and not node.right:
                seq.append(node.val)
            dfs(node.left, seq)
            dfs(node.right, seq)

        seq1 = []
        seq2 = []
        dfs(root1, seq1)
        dfs(root2, seq2)

        return seq1 == seq2

1448. Count Good Nodes in Binary Tree

class Solution:
    def goodNodes(self, root: TreeNode) -> int:
        
        # Intuition:
        # As we traverse the BST, store the max value seen so far,
        # and carry it in our recursive calls.

        good_nodes = []  # allows multiple writes at a time

        def isGoodNode(node: TreeNode, max_val: int):
            if not node:
                return False
            if node.val >= max_val:
                good_nodes.append(1)
                max_val = node.val
            isGoodNode(node.left, max_val)
            isGoodNode(node.right, max_val)
        
        isGoodNode(root, root.val)  # recursive calls inside

        return sum(good_nodes)

1448. Count Good Nodes in Binary Tree

class Solution:
    def goodNodes(self, root: TreeNode) -> int:
        
        # Intuition:
        # As we traverse the BST, store the max value seen so far,
        # and carry it in our recursive calls.

        good_nodes = []  # allows multiple writes at a time

        def isGoodNode(node: TreeNode, max_val: int):
            if not node:
                return False
            if node.val >= max_val:
                good_nodes.append(1)
                max_val = node.val
            isGoodNode(node.left, max_val)
            isGoodNode(node.right, max_val)
        
        isGoodNode(root, root.val)  # recursive calls inside

        return sum(good_nodes)

437. Path Sum III

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:

        # TODO

        # Helper function: DFS that carries the current sum
        def dfs(node, current_sum):
            if not node:
                return 0

            current_sum += node.val

            num_paths = prefix_sums.get(current_sum - targetSum, 0)

            prefix_sums[current_sum] = prefix_sums.get(current_sum, 0) + 1  # update

            # Recursive call on `left` and `right`:
            num_paths += dfs(node.left, current_sum)
            num_paths += dfs(node.right, current_sum)

            # Remove the current sum from the prefix sums when backtracking:
            prefix_sums[current_sum] -= 1

            return num_paths

        prefix_sums = {0: 1}  # Base case: A sum of zero is seen once

        return dfs(root, 0)

1372. Longest ZigZag Path in a Binary Tree

class Solution:
    def longestZigZag(self, root: Optional[TreeNode]) -> int:

        # Intuition: Use DFS to recursively update the lengths of ZigZag paths.

        # `direction`: starting at the given `node`, where to go next
        def dfs(node, direction, length):
            if not node:
                return
            
            nonlocal max_length  # "pass by reference" for integers in Python
            max_length = max(max_length, length)
            
            if direction == "left":
                dfs(node.left, "right", length + 1)
                dfs(node.right, "left", 1)  # length reset to 1 since we are told to go "left" next but actually go "right" instead
            else:
                dfs(node.right, "left", length + 1)
                dfs(node.left, "right", 1)  # length reset to 1
    
        max_length = 0
        dfs(root.left, "right", 1)  # starts at `root.left`; should go right next; has a length of 1 from `root` to `root.left`
        dfs(root.right, "left", 1)
        return max_length

236. Lowest Common Ancestor of a Binary Tree

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        
        # Intuition:
        # By observation, we are able to identify the LCA of p and q only if:
        #   1) p and q are separately located in the left or right subtrees of a node
        #   2) p is an ancestor of q (or the other way around)

        # Base case 1: we've reached a leaf node
        if not root:  
            return None

        # Base case 2: Case 2, Intuition
        if root == p or root == q:  
            return root
        
        # Recursive calls:
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)

        # Base case 3: Case 1, Intuition
        if left and right:
            return root

        # Base case 4: we keep searching in the left/right subtree
        return left if left else right

Binary Tree - BFS

199. Binary Tree Right Side View

class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:

        # Intuition: The solution will be very similar to
        # [102. Binary Tree Level Order Traversal],
        # except that we only need to store the right-most node at each level.

        # Base case handling:
        if not root:
            return []

        queue = deque([root])
        result = []
        
        while queue:
            num_nodes = len(queue)  # takes a snapshot of the queue size
            # node_values = []  # all nodes at current level
            right_most_val = None

            # Pop until the original queue becomes empty:
            for _ in range(num_nodes):
                # 1. Visit:
                node = queue.popleft()  # breadth-first visiting
                # node_values.append(node.val)
                right_most_val = node.val
                # 2. Expand:
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            
            # Store the right-most node at this level:
            # result.append(node_values[-1])
            result.append(right_most_val)

        return result

1161. Maximum Level Sum of a Binary Tree

class Solution:
    def maxLevelSum(self, root: Optional[TreeNode]) -> int:

        # Intuition:
        # Since we need the sums of nodes _by level_, we run BFS using a FIFO queue (`deque`).

        queue = deque([root])
        max_sum = float('-inf')
        min_level = 1

        level = 1
        while queue:
            # Compute the level sum:
            level_sum = sum(node.val for node in queue)
            if level_sum > max_sum:
                max_sum = level_sum
                min_level = level

            # Get nodes of the next level:
            for _ in range(len(queue)):
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            level += 1

        return min_level

Binary Search Tree

700. Search in a Binary Search Tree

class Solution:
    def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        # Handle recursive calls on None:
        if not root:
            return None

        if val == root.val:
            return root  # base case
        elif val < root.val:
            return self.searchBST(root.left, val)  # search in the left subtree
        else:
            return self.searchBST(root.right, val)  # search in the right subtree

450. Delete Node in a BST

class Solution:
    def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:

        # Intuition:
        # Finding the node is easy, but when deleting it (if it has child nodes), we have to make sure the properties of BST still hold.

        if not root:
            return None

        # Search and delete:
        if key < root.val:  # search in the left subtree
            root.left = self.deleteNode(root.left, key)
        elif key > root.val:  # search in the right subtree
            root.right = self.deleteNode(root.right, key)
        else:  # delete the current node:

            # Case 1: No child or one child
            if not root.left:
                return root.right
            elif not root.right:
                return root.left

            # Case 2: The node has two children.

            # To maintain a BST, we should replace this node with the smallest in its right subtree.
            # Alternatively, we could also replace this node with the largest in its left subtree.

            smallest_right = self.getMin(root.right)
            root.val = smallest_right.val
            root.right = self.deleteNode(root.right, smallest_right.val)
            # We delete that smallest node in the right subtree by using a deleteNode() call.
        
        return root

    # getMin() returns the smallest node in a given BST
    def getMin(self, node: TreeNode) -> TreeNode:
        while node.left:
            node = node.left
        return node