Understand what the interviewer is asking for by using test cases and questions about the problem.
- Established a set (2-3) of test cases to verify their own solution later.
- Established a set (1-2) of edge cases to verify their solution handles complexities.
- Have fully understood the problem and have no clarifying questions.
- Have you verified any Time/Space Constraints for this problem?
HAPPY CASE
Input: root = [1,2,5,3,4,null,6]
Output: [1,null,2,null,3,null,4,null,5,null,6]
Input: root = [1,3,2,5,3,null,9]
Output: [1,3,9]
Input: root = [1,2,3]
Output: [1,3]
EDGE CASE
Input: root = []
Output: []
Input: root = [0]
Output: [0]
Match what this problem looks like to known categories of problems, e.g. Linked List or Dynamic Programming, and strategies or patterns in those categories.
For trees, some things we should consider are:
Plan the solution with appropriate visualizations and pseudocode.
1) Traverse the tree via a post-order traversal
2) When we visit a node, recursively flatten its left subtree (if it exists), then its right subtree (if it exists)
3) To correctly arrange the flattened subtrees, the current node's next pointer (in this case we use the right pointer as a next pointer)
should be set to the head of the left flattened subtree, then the tail of the left flattened subtree should be set to the head of the right flattened subtree
- There are some special cases to consider:
- If there is no right subtree, then the tail of the left subtree should be connected to NULL
- If there is no left subtree, the current node should be connected directly to the head of the right flattened subtree
(Luckily for us, this is how the tree is already setup, so no action needs to be taken in this case.)
- If there is neither a left nor right subtree, then the current node should be connected to NULL
(This is how the tree is already setup when there are no subtrees at the current node, so again no action needs to be taken.)
4) Based on step 3, we need to be able to keep track of the head and tail of a flattened portion of the tree, and return them up the call stack as we recurse
- The head of the flattened subtree will be the current node we are visiting
- The tail of the flattened subtree will vary depending on what subtrees were visited:
- If there was a right subtree, the tail will be the tail of the right flattened subtree
- If there was no right subtree but there was a left subtree, the tail will be the tail of the left flattened subtree
- If there was neither a left nor right subtree, the tail will just be the current node
⚠️ Common Mistakes
Implement the code to solve the algorithm.
class Solution {
// stores root of branch that was previously flattened
private TreeNode prevRoot = null;
// recursively flattens using post-order traversal
// visits each node once: O(n) time, O(h) space
public void flatten(TreeNode root) {
if (root == null) return;
// flattens subtrees in post-order
// this ensures flattened right sub-list comes after the left sub-list
flatten(root.right);
flatten(root.left);
// after children are flattened, just set head of sub-list as child
root.right = prevRoot;
root.left = null;
prevRoot = root
}
}
def flatten(root: Optional[TreeNode]) -> None:
def getList(node):
# initialize head and tail of left flattened subtree to None
leftHead = None
leftTail = None
# if the left subtree exists, recursively flatten and get the
# head and tail of the flattened subtree
if node.left:
(leftHead, leftTail) = getList(node.left)
# initialize head and tail of right flattened subtree to None
rightHead = None
rightTail = None
# if the right subtree exists, recursively flatten and get the
# head and tail of the flattened subtree
if node.right:
(rightHead, rightTail) = getList(node.right)
# we have variables pointing to the left and right subtrees, so we can
# safely set the node's left to None
node.left = None
# if there is a left subtree, we need to set the node's next (right) pointer
# to point to the head of the flattened left subtree
# We also need to set the tail of the left flattened subtree to be the head
# of the right flattened subtree.
# (Note: since we initialized rightHead to None above, this will give the
# correct assignment even if there is no right subtree)
if leftHead:
node.right = leftHead
leftTail.right = rightHead
# based on the presence of flattened subtrees, we determine the head and tail of the
# flattened portion of the tree
# - The head will always be the current node
# - The tail will either be the rightTail, leftTail, or the current node itself,
# depending on what is defined
if rightHead:
return (node, rightTail)
elif leftHead:
return (node, leftTail)
else:
return (node, node)
# handle null root edge case
if not root:
return root
# return the head of the flattened tree
return getList(root)[0]
Review the code by running specific example(s) and recording values (watchlist) of your code's variables along the way.
Evaluate the performance of your algorithm and state any strong/weak or future potential work.
Assume N
represents the number of nodes in the tree.