Binary Tree
Notes
class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right
Breadth-First Traversal (BFS)
from collections import deque def bfs(root): if not root: return [] result = [] # If deque is not allowed, use lists to store current and next level queue = deque([root]) while queue: curr = queue.popleft() result.append(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) return result
Preorder traversal
- Used to create a copy of the tree
def preorderRecursive(root): if not root: return print(root.val) preorderRecursive(root.left) preorderRecursive(root.right) def preorderIterative(root): stack = [root] while stack: node = stack.pop() print(node.val) # Push node.right first because stack is LIFO if node.right: stack.append(node.right) if node.left: stack.append(node.left)
Inorder traversal
- Used to get sorted values in a binary search tree
def inorderRecursive(root): if not root: return preorderRecursive(root.left) print(root.val) preorderRecursive(root.right) def inorderIterative(root): curr, stack = root, [] while curr or stack: # Always try to get leftmost child node while curr: stack.append(curr) curr = curr.left curr = stack.pop() print(curr.val) curr = curr.right
Postorder traversal
- Used to delete a tree starting from leaves to root
def postorderRecursive(root): if not root: return preorderRecursive(root.left) preorderRecursive(root.right) print(root.val) def postorderIterative(root): stack, result = [root], [] # Preorder is root -> left -> right # Modify preorder to root -> right -> left, # then reverse result: left -> right -> root (== postorder) while stack: node = stack.pop() result.append(node.val) # Push node.left first, opposite of preorder solution if node.left: stack.append(node.left) if node.right: stack.append(node.right) result.reverse() for node in result: print(node.val)
Maximum Depth of Binary Tree (Easy)
Given the root of a binary tree, return its maximum depth.
A binary tree's maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.
def maxDepth(root: Optional[TreeNode]) -> int: if not root: return 0 left = self.maxDepth(root.left) + 1 right = self.maxDepth(root.right) + 1 return max(left, right)
Time: O(n)
Space: O(h), h = tree height
Same Tree (Easy)
Given the roots of two binary trees p and q, write a function to check if they are the same or not.
Two binary trees are considered the same if they are structurally identical, and the nodes have the same value.
def isSameTree(p: Optional[TreeNode], q: Optional[TreeNode]) -> bool: if not p and not q: return True if (not p or not q) or p.val != q.val: return False left = self.isSameTree(p.left, q.left) right = self.isSameTree(p.right, q.right) return left and right
Time: O(n)
Space: O(h), h = tree height
Invert Binary Tree (Easy)
Given the root of a binary tree, invert the tree, and return its root.
Recursive:
def invertTree(root: Node) -> Node: if not root: return root left = self.invertTree(root.left) right = self.invertTree(root.right) root.left, root.right = root.right, root.left return root
Time: O(n)
Space: O(h), h = tree height
Iterative:
from collections import deque def invertTree(root: Node) -> Node: if not root: return root q = deque() q.append(root) while q: node = q.popleft() node.left, node.right = node.right, node.left if node.left: q.append(node.left) if node.right: q.append(node.right) return root
Time: O(n)
Space: O(n)
Symmetric Tree (Easy)
Given the root of a binary tree, check whether it is a mirror of itself (i.e., symmetric around its center).
Recursive:
def isSymmetric(root: Node) -> bool: def helper(left, right) -> bool: if not left and not right: return True if not left or not right: return False if left.val != right.val: return False outer = helper(left.left, right.right) inner = helper(left.right, right.left) return outer and inner if not root: return False return helper(root.left, root.right)
Time: O(n)
Space: O(h), h = tree height
Iterative:
from collections import deque # Time complexity: O(n) # Space complexity: O(n) def isSymmetric(root:Node) -> bool: if not root: return False q = deque() q.append(root.left) q.append(root.right) while q: left = q.popleft() right = q.popleft() if not left and not right: continue if not left or not right: return False if left.val != right.val: return False q.append(left.left) q.append(right.right) q.append(left.right) q.append(right.left) return True
Path Sum (Easy)
Given the root of a binary tree and an integer targetSum, return true if the tree has a root-to-leaf path such that adding up all the values along the path equals targetSum.
A leaf is a node with no children.
def hasPathSum(root: Optional[TreeNode], targetSum: int) -> bool: def helper(node, target, total): if not node: return False if not node.left and not node.right: return target == total + node.val left = helper(node.left, target, total + node.val) right = helper(node.right, target, total + node.val) return left or right return helper(root, targetSum, 0)
Time: O(n)
Space: O(h), h = tree height
Count Complete Tree Nodes (Easy)
Given the root of a complete binary tree, return the number of the nodes in the tree.
In a complete binary tree, every level, except possibly the last, is completely filled in a complete binary tree, and all nodes in the last level are as far left as possible. It can have between 1 and 2h nodes inclusive at the last level h.
Design an algorithm that runs in less than O(n) time complexity.
# Time complexity: O((log n)^2) # Space complexity: O(h), h = height of tree def countNodes(root: Optional[TreeNode]) -> int: # getHeight() is O(log n) assuming complete binary tree # In a complete binary tree, n = h^2 def getHeight(root): if not root: return 0 return getHeight(root.left) + 1 if not root: return 0 left = getHeight(root.left) right = getHeight(root.right) # If equal subtree heights, left subtree is perfect binary tree if left == right: return countNodes(root.right) + 2**left # Else right subtree is perfect binary tree with one less height else: return countNodes(root.left) + 2**right
Time: O(log2n) assuming complete binary tree
Space: O(h), h = tree height
Construct Binary Tree from Preorder and Inorder Traversal (Medium)
Given two integer arrays preorder and inorder where preorder is the preorder traversal of a binary tree and inorder is the inorder traversal of the same tree, construct and return the binary tree.
preorder and inorder consist of unique values.
# In a preorder list, a node's left child is always the next element in the list # In an inorder list, left side elements are left subtree of current node. Same with right def buildTree(preorder: List[int], inorder: List[int]) -> Optional[TreeNode]: # Map inorder indices for O(1) lookup m = {} for i in range(len(inorder)): m[inorder[i]] = i # Recursively build left and right child nodes while shrinking list def helper(preI, inLeft, inRight): if preI >= len(preorder) or inLeft > inRight: return None inI = m[preorder[preI]] node = TreeNode(preorder[preI]) node.left = helper(preI + 1, inLeft, inI - 1) # To get the current node's right child's index in preorder, # Skip the length of the entire left subtree # Left subtree length = inI - inLeft + 1 node.right = helper(preI + inI - inLeft + 1, inI + 1, inRight) return node return helper(0, 0, len(inorder) - 1)
Time: O(n)
Space: O(n)
Construct Binary Tree from Inorder and Postorder Traversal (Medium)
Given two integer arrays inorder and postorder where inorder is the inorder traversal of a binary tree and postorder is the postorder traversal of the same tree, construct and return the binary tree.
The solution is similar to "Construct Binary Tree from Preorder and Inorder Traversal". Notice that the output of postorder traversal is similar to preorder traversal, except postorder starts with right subtrees and the output is reversed.
def buildTree(inorder: List[int], postorder: List[int]) -> Optional[TreeNode]: m = {} for i in range(len(inorder)): m[inorder[i]] = i def helper(postI, inLeft, inRight): if postI < 0 or inLeft > inRight: return None inI = m[postorder[postI]] node = TreeNode(postorder[postI]) # To get the current node's left child's index in postorder, # Skip the length of the entire right subtree # Right subtree length = inRight - inI + 1 node.left = helper(postI - (inRight - inI + 1), inLeft, inI - 1) node.right = helper(postI - 1, inI + 1, inRight) return node return helper(len(postorder) - 1, 0, len(inorder) - 1)
Time: O(n)
Space: O(n)
Populating Next Right Pointers in Each Node II (Medium)
Given a binary tree with nodes:
class Node: def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None): self.val = val self.left = left self.right = right self.next = next
Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL.
Initially, all next pointers are set to NULL.
def connect(root: 'Node') -> 'Node': if not root: return root # Iterative breadth first traversal currLevel = [root] while currLevel: nextLevel = [] for node in currLevel: if node.left: nextLevel.append(node.left) if node.right: nextLevel.append(node.right) for i in range(len(nextLevel) - 1): nextLevel[i].next = nextLevel[i + 1] currLevel = nextLevel return root
Time: O(n)
Space: O(n)
Follow-up: Use only constant space. Recursion using implicit stack space is fine.
def connect(root: 'Node') -> 'Node': # Treat each level as a linked list curr = root dummy = tail = Node() while curr: # Build pointers for next level if curr.left: tail.next = curr.left tail = tail.next if curr.right: tail.next = curr.right tail = tail.next curr = curr.next # Reached end of current level. Set pointers for next level # dummy.next is first node of next level if not curr: curr = dummy.next tail = dummy dummy.next = None return root
Time: O(n)
Space: O(1)
Flatten Binary Tree to Linked List (Medium)
Given the root of a binary tree, flatten the tree into a "linked list":
- The "linked list" should use the same
TreeNodeclass where therightchild pointer points to the next node in the list and theleftchild pointer is alwaysnull. - The "linked list" should be in the same order as a pre-order traversal of the binary tree.
def flatten(root: Optional[TreeNode]) -> None: if not root: return # Make preorder list of nodes, then update node pointers nodes = [] def helper(root): if not root: return nodes.append(root) helper(root.left) helper(root.right) helper(root) for i in range(len(nodes) - 1): nodes[i].left = None nodes[i].right = nodes[i + 1]
Time: O(n)
Space: O(n)
Follow-up: Use O(1) space.
def flatten(root: Optional[TreeNode]) -> None: # Remove the right subtree and attach to the rightmost node # in the left subtree. This preserves preorder curr = root while curr: if curr.left: # Find rightmost node in left subtree rightMostNode = curr.left while rightMostNode.right: rightMostNode = rightMostNode.right # Attach right subtree rightMostNode.right = curr.right # Move left subtree to right side curr.right = curr.left curr.left = None curr = curr.right
Time: O(n)
Space: O(1)
Sum Root to Leaf Numbers (Medium)
You are given the root of a binary tree containing digits from 0 to 9 only.
Each root-to-leaf path in the tree represents a number. For example, the root-to-leaf path 1 -> 2 -> 3 represents the number 123.
Return the total sum of all root-to-leaf numbers. A leaf node is a node with no children.
def sumNumbers(root: Optional[TreeNode]) -> int: paths = [] def helper(node, pathVal): if not node: return pathVal = pathVal * 10 + node.val if not node.left and not node.right: paths.append(pathVal) return helper(node.left, pathVal) helper(node.right, pathVal) helper(root, 0) return sum(paths)
Time: O(n)
Space: O(h), h = tree height
Binary Search Tree Iterator (Medium)
Implement the BSTIterator class that represents an iterator over the in-order traversal of a binary search tree (BST):
BSTIterator(TreeNode root)Initializes an object of theBSTIteratorclass. Therootof the BST is given as part of the constructor. The pointer should be initialized to a non-existent number smaller than any element in the BST.boolean hasNext()Returnstrueif there exists a number in the traversal to the right of the pointer, otherwise returnsfalse.int next()Moves the pointer to the right, then returns the number at the pointer.
Notice that by initializing the pointer to a non-existent smallest number, the first call to next() will return the smallest element in the BST.
You may assume that next() calls will always be valid. That is, there will be at least a next number in the in-order traversal when next() is called.
Implement next() and hasNext() to run in average O(1) time and use O(h) memory, where h is the height of the tree.
class BSTIterator: def __init__(self, root: Optional[TreeNode]): self.stack = [] self.node = root # Iterative inorder traversal def next(self) -> int: # Get leftmost node # This loop runs at most n times. # If next() is called n times, average time complexity is O(1) while self.node: self.stack.append(self.node) self.node = self.node.left nextNode = self.stack.pop() # If nextNode.right is None, get next node from stack self.node = nextNode.right return nextNode.val def hasNext(self) -> bool: return bool(self.stack or self.node)
Time: O(1) average
Space: O(h), h = tree height
Lowest Common Ancestor of a Binary Tree (Medium)
Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).
def lowestCommonAncestor(root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': if not root: return None # Return early if p/q found because LCA cannot be lower # If p/q is LCA, recursion will exit with p/q if root == p or root == q: return root left = lowestCommonAncestor(root.left, p, q) right = lowestCommonAncestor(root.right, p, q) # LCA is current node if left and right: return root # Answer will be carried up recursion stack return left or right
Time: O(n)
Space: O(h), h = tree height
Average of Levels in Binary Tree (Easy)
Given the root of a binary tree, return the average value of the nodes on each level in the form of an array.
def averageOfLevels(root: Optional[TreeNode]) -> List[float]: currLvl, answer = [root], [] while currLvl: nextLvl, total = [], 0 for node in currLvl: total += node.val if node.left: nextLvl.append(node.left) if node.right: nextLvl.append(node.right) answer.append(total / len(currLvl)) currLvl = nextLvl return answer
Time: O(n)
Space: O(n)
Binary Tree Right Side View (Medium)
Given the root of a binary tree, imagine yourself standing on the right side of it, and return the values of the nodes you can see ordered from top to bottom.
def rightSideView(root: Optional[TreeNode]) -> List[int]: if not root: return [] # Iterative breadth first traversal currLvl, answer = [root], [] while currLvl: answer.append(currLvl[-1].val) nextLvl = [] for node in currLvl: if node.left: nextLvl.append(node.left) if node.right: nextLvl.append(node.right) currLvl = nextLvl return answer
Time: O(n)
Space: O(n)
Alternative answer:
def rightSideView(root: Optional[TreeNode]) -> List[int]: answer = [] # Reversed preorder traversal def helper(root, depth: int) -> None: if not root: return if depth == len(answer): answer.append(root.val) helper(root.right, depth + 1) helper(root.left, depth + 1) helper(root, 0) return answer
Time: O(n)
Space: O(h), h = tree height
Binary Tree Level Order Traversal (Medium)
Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level).
def levelOrder(root: Optional[TreeNode]) -> List[List[int]]: if not root: return [] currLvl, answer = [root], [] while currLvl: answer.append([]) nextLvl = [] for node in currLvl: answer[-1].append(node.val) if node.left: nextLvl.append(node.left) if node.right: nextLvl.append(node.right) currLvl = nextLvl return answer
Time: O(n)
Space: O(n)
Binary Tree Zigzag Level Order Traversal (Medium)
Given the root of a binary tree, return the zigzag level order traversal of its nodes' values. (i.e., from left to right, then right to left for the next level and alternate between).
def zigzagLevelOrder(root: Optional[TreeNode]) -> List[List[int]]: if not root: return [] currLvl, answer, forward = [root], [], True while currLvl: vals = [] for node in currLvl: vals.append(node.val) if not forward: vals.reverse() answer.append(vals) forward = not forward nextLvl = [] for node in currLvl: if node.left: nextLvl.append(node.left) if node.right: nextLvl.append(node.right) currLvl = nextLvl return answer
Time: O(n)
Space: O(n)
Minimum Absolute Difference in BST (Easy)
Given the root of a Binary Search Tree (BST), return the minimum absolute difference between the values of any two different nodes in the tree.
def getMinimumDifference(root: Optional[TreeNode]) -> int: answer, prev = float("inf"), None # Inorder traversal, keep track of prev def helper(root): if not root: return helper(root.left) nonlocal answer, prev if prev: answer = min(answer, root.val - prev.val) prev = root helper(root.right) helper(root) return answer
Time: O(n)
Space: O(h), h = tree height
Kth Smallest Element in a BST (Medium)
Given the root of a binary search tree, and an integer k, return the kth smallest value (1-indexed) of all the values of the nodes in the tree.
def kthSmallest(root: Optional[TreeNode], k: int) -> int: # Iterative inorder traversal curr, stack = root, [] while stack or curr: while curr: stack.append(curr) curr = curr.left node = stack.pop() k -= 1 if k == 0: return node.val curr = node.right return -1
Time: O(n)
Space: O(h), h = tree height
Alternative solution:
def kthSmallest(root: Optional[TreeNode], k: int) -> int: count, answer = 0, 0 # Recursive inorder traversal, counting nodes def helper(root): if not root: return helper(root.left) nonlocal count, answer count += 1 if count == k: answer = root.val return helper(root.right) helper(root) return answer
Time: O(n)
Space: O(h), h = tree height
Validate Binary Search Tree (Medium)
Given the root of a binary tree, determine if it is a valid binary search tree (BST).
A valid BST is defined as follows:
- The left subtree of a node contains only nodes with keys less than the node's key.
- The right subtree of a node contains only nodes with keys greater than the node's key.
- Both the left and right subtrees must also be binary search trees.
def isValidBST(root: Optional[TreeNode]) -> bool: stack, prev, curr = [], None, root # Iterative inorder traversal, tracking prev while stack or curr: while curr: stack.append(curr) curr = curr.left node = stack.pop() if prev and prev.val >= node.val: return False prev = node curr = node.right return True
Time: O(n)
Space: O(h), h = tree height
Alternative solution:
def isValidBST(root: Optional[TreeNode]) -> bool: # Recursive inorder traversal, updating valid value range def helper(root, minV, maxV): if not root: return True if root.val <= minV or root.val >= maxV: return False left = helper(root.left, minV, root.val) right = helper(root.right, root.val, maxV) return left and right return helper(root, float("-inf"), float("inf"))
Time: O(n)
Space: O(h), h = tree height