Common LeetCode Algorithms
- I. Binary Search
- II. Binary Tree & Divide and Conquer
- III. BFS & Topological Sorting
- IV. DFS
- V. Graph
- VI. Two Pointer & Linked List
- VII. Sliding Window & Sweep Line
- VIII. Heap & Top K & Mono Stack
- IX. Prefix Sum & Stack & DP
Introduction
Both Traversal and Divide and Conquer are iteration methods that can be written using either recursion or iteration, though recursion is more commonly used.
- Traversal involves visiting each node in the tree, typically using a Depth-First Search (DFS) method. It often utilizes a global variable passed down during the traversal.
- Divide and Conquer usually does not use a global variable. It processes the return values of the left and right children to determine the current node’s return value, focusing on building a return value from the children’s values.
Most tree problems can be effectively solved using the Divide and Conquer approach.
Three traversal notions:
- Preorder: root, left, right
- Postorder: left, right, root
- Inorder: left, root, right Note: Left and right can be either nodes or subtrees.
Binary Tree Search Pattern (Divide and Conquer)
|
|
The leaf node processing is optional. Sometimes the leftReturn and rightReturn of a leaf node’s children (both None) can be processed to get the desired value. However, sometimes the return of None children is just None, and we need to specify what should be returned from two None returns.
Tree Traversal Type
144. Binary Tree Preorder Traversal
In this traversal, we pass a result variable as an argument during recursion, updating it with the current root’s value, then with the left subtree, and finally with the right subtree.
The time complexity is O(n), as each node in the tree is visited exactly one. The space complexity is O(logn), corresponding to the depth of the function call stack (the height of the tree).
In the following questions using Divide and Conquer to traverse the tree, unless specified otherwise, the time complexity and space complexity remain the same as tree traversal (O(n) and O(logn)).
|
|
Alternatively, we can keep the result as a global variable and update it during the recursion.
|
|
94. Binary Tree Inorder Traversal
Similar to 144. Binary Tree Preorder Traversal, but we update the result with the left subtree first, then the current root, and finally the right subtree.
|
|
145. Binary Tree Postorder Traversal
Similar to 144. Binary Tree Preorder Traversal, but we update the result with the left subtree first, then the right subtree, and finally the current root.
|
|
104. Maximum Depth of Binary Tree
The children should return their depths, and the root should compare the depths and add 1 to determine its depth. The maximum depth of the tree is the depth of the root node.
|
|
1120. Maximum Average Subtree
To calculate the new average of the current subtree, we need to know the sum and count. We should also compare the current max with its children’s max. Thus, the children should return both the sum, count, and max_average of nodes in the subtree.
The root node should calculate the new average and new max_average from returns. Additionally, we can maintain a max_average global variable, and the children should return both sum and count.
|
|
110. Balanced Binary Tree
A balanced tree means any node in the subtrees should have a height difference no larger than 1. We can maintain a global variable to indicate whether the tree is balanced. The children should return their root’s height for comparison.
The root should check if the height difference between the two subtrees is greater than 1, and if so, set the global variable to False. The root should return its own height (the maximum of the subtrees’ heights plus 1).
|
|
236. Lowest Common Ancestor of a Binary Tree
If both P and Q are found from the children, then the root is the ancestor. We can use a global variable to record the found ancestor. Children should return whether they contain P or Q.
The root should check whether both children are true, or whether both the root and one child are true (i.e., whether two nodes out of three (root, children) are true (P or Q)).
|
|
Another solution is to return the node that is P or Q directly.
|
|
114. Flatten Binary Tree to Linked List
To flatten the binary tree into a linked list-like binary tree, we should move the left subtree to the right, and move the original right subtree to the position after the end of the original left subtree.
The return from children should track the last one node of the subtree. Upon receiving returns, the current root should follow these steps:
leftReturn.right = root.right
root.right = root.left
root.left = None
Finally, return the last node of the current tree (rightReturn if it exists, otherwise leftReturn, otherwise the root node).
|
|
101. Symmetric Tree
A symmetric tree is symmetric around the root node, so we should consider each pair of subtrees rather than a single subtree. For a pair of subtrees, it is symmetric if:
- The roots are equal.
- The left and right subtrees are symmetric: the left child of the left root equals the right child of the right root, and the right child of the left root equals the left child of the right root.
Thus, we should return a boolean from children and return False if either child returns False or the roots are not equal.
|
|
Path Related Types
257. Binary Tree Paths
The children should return all paths that have them as the roots. The root should build its paths from leftReturn and rightReturn by adding the root value and "->"
to each element of them. For a leaf node, directly return its own value string in the list.
|
|
112. Path Sum
The children should return a list of path sums. The root should add its value to each value in the left and right returns to build a new list. The target is found in the root’s final list of values.
|
|
549. Binary Tree Longest Consecutive Sequence II
For a root node, the longest consecutive length can be an extension of the left or right subtree, or it can combine two subtrees as the bridge. The key is to identify the consecutively increasing or decreasing path from the children.
Since the result is the longest length, we can return the longest increase length (ending with the root) and the longest decrease length (starting with the root) from the children. The root should compare several situations’ lengths:
- left increase + 1 (root)
- left decrease + 1 (root)
- 1 (root) + right increase
- 1 (root) + right decrease
At each level, we compare 1 and 3 to get the longest increase length, and compare 2 and 4 to get the longest decrease length. These two values will be passed to the upper level. Additionally, we should calculate increase + decrease - 1 (root)
to update the longest sequence global variable.
|
|
124. Binary Tree Maximum Path Sum
Similar to 549. Binary Tree Longest Consecutive Sequence II, the path does not necessarily go through the root. For each level, we should compare not only the path ending at the root, but also the path passing by the root. As node values can be negative, we should compare several situations:
- root
- left max
- left max + root
- right max
- right max + root
- left max + root + right max
At each level, we compare 1, 3, and 5 to get the maximum sum that ends with the root, which will be passed to the upper level. Additionally, we should consider 2, 4, and 6, where the root is not included or included but passed by, to update the maximum sum global variable.
|
|
129. Sum Root to Leaf Numbers
Similar to 112. Path Sum, but recording the path node values rather than adding them together. We can use a list to record nodes in each path and process the list to get the required value. However, an easier way is to record nodes in a path as a string (from root to leaf) and transfer the string directly to the required value.
Children should return a list of strings, each representing a path. The root should add its own value at the beginning of each string.
|
|
Binary Search Tree (BST) Types
98. Validate Binary Search Tree
For a valid Binary Search Tree (BST), each root should be larger than the max of the left subtree and smaller than the min of the right subtree. Thus, children should return (min, max) and maintain a global isValid variable. The root should make the comparison and set the variable to False if the condition is invalid.
|
|
1373. Maximum Sum BST in Binary Tree
For a valid Binary Search Tree (BST), each root should be larger than the max of the left subtree and smaller than the min of the right subtree. Thus, children should return min and max of the subtree for validation. They should also return a sum to calculate the new sum. Additionally, we should maintain a global variable to record the maximum sum during DFS. The root should calculate and return new (min, max, sum).
|
|
Summary
The key to using the Divide and Conquer method to solve binary tree-related problems lies in determining what should be returned from the children and how the root should process this return information to build its own return value.
A global variable is optional, but it can be useful for certain problems, especially those involving finding minimum or maximum values (e.g., maximum path sum, maximum average subtree). The global variable helps keep track of the desired value during the recursion.
The corner case happens at the leaf nodes and their children (which are None).Special attention should be given to them, and we should carefully build the return value for these cases, or explicitly indicate return values for leaf nodes (optional).
Last modified on 2024-06-15