Binary Tree, LeetCode

Full Binary Tree (Proper Binary Tree)

  • every node has either exactly two children or no children.

Perfect Binary Tree (Strictly Full Binary Tree):

  • Every internal node has two children
  • All leaf nodes appear at the same depth(level).
  • Level (k) start from 0
    • Every level has 2^k nodes (Level(k) start from 0).
    • Total nodes: 2^(k+1) - 1.
  • Level (k) start from 1
    • Every level has 2^(k - 1). nodes (Level(k) start from 0)
    • Total nodes: 2^(k) - 1.

Complete Binary Tree:

  • Every level, except possibly the last, is completely filled.
  • In the last level, all nodes are as far left as possible, with no “gaps” between them.
  • heap is a complete binary tree.

Binary Search Tree:

  • Average case for search and insert: O(log n) when the tree is approximately balanced (each comparison eliminates about half of the remaining nodes).
  • Worst case for search and insert: O(n) if the tree becomes completely unbalanced (degenerates into a chain).
  • Left -> Small, Top -> Medium, Right -> Big
  • No requirement for node structure

Balanced Binary Search Tree:

  • ∀ N: height(N.left) – height(N.right) ≤ 1
  • the basic of map and set (the key in map and the element in set is in order)
# Initialization by Linked List:
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

# Initialization by List:
list = []
curr: list[index] 
left_children: list[2 * index + 1]
right_children: list[2 * index + 2]

Using backtracking to build binary tree

class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

nums = [1, 2, 3, 4, 5]
def list_to_complete_binary_tree(nums: List[int], i = 0):
    if i > len(nums) - 1:
        return
    node = ListNode(nums[i])
    node.left = list_to_complete_binary_tree(nums, i * 2 + 1)
    node.right = list_to_complete_binary_tree(nums, i * 2 + 2)
    return node

DFS:

# result should be the input of dfs, so that every recursive call will write into the same list
# pre-order
def dfs(self, result, root):
    if not root:
        return
    result.append(root.val) # middle
    left_branch = dfs(result, root.left) # left
    right_branch = dfs(result, root.right) # right
    return result

BFS

from collections import deque
def bfs(self, root):
    if not root:
        return []
    queue = deque([root]) # queue stores node instead of the value of node
    result = []
    while queue:
        length_of_level = len(queue)
        level = []
        for i in range(length_of_level):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level)
    return result

144. Binary Tree Preorder Traversal

# 1: _dfs accept result as input
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

class Solution:
    def preorderTraversal(self, root: Optional[List]) -> List[int]:
        return self._dfs(result = [], root = root) if root else []
    
    def _dfs(self, result: List[int], root: Optional[List]) -> List[int]: 
        if not root:
            return # stop iteration
        result.append(root.val)
        left_branch = self._dfs(result, root.left)
        right_branch = self._dfs(result, root.right)
        return result

# 2. _dfs doesn't accept result as input
class Solution:
    def preorderTraversal(self, root: Optional[List]) -> List[int]:
        if not root:
            return [] # return value
        left = self.preorderTraversal(root.left) # return the value of left_branch
        right = self.preorderTraversal(root.right)
        return [root.val] + left + right # left and right are all list 

145. Binary Tree Postorder Traversal

from typing import Optional, List
class ListNode:
    def __ini__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
    
class Solution:
    def postorderTraversal(self, root: Optional[ListNode]) -> ListNode:
        if not root:
            return []
        left = self.postorderTraversal(root.left)
        right = self.postorderTraversal(root.right)
        return left + right from typing import Optional, List
class ListNode:
    def __ini__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
    
class Solution:
    def postorderTraversal(self, root: Optional[ListNode]) -> ListNode:
        if not root:
            return []
        left = self.postorderTraversal(root.left)
        right = self.postorderTraversal(root.right)
        return left + right + [root.val]
# Or
class Solution:
    def postorderTraversal(self, root: Optional[ListNode]) -> ListNode:
        return self._dfs(result = [], root = root) if root else [] # in case _dfs only run the first two line code
    
    def _dfs(self, result, root) -> List[int]:
        if not root:
            return
        left_branch = self._dfs(result, root.left)
        right_branch = self._dfs(result, root.right)
        result.append(root.val)
        return result

94. Binary Tree Inorder Traversal

from typing import List, Optional
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.right = right
        self.left = left

class Solution:
    def inorderTraversal(self, root: Optional[ListNode]) -> List[int]:
        return self._dfs(result = [], root = root) if root else []

    def _dfs(self, result, root) -> List[int]:
        if not root:
            return
        left_branch = self._dfs(result, root.left)
        result.append(root.val)
        right_branch = self._dfs(result, root.right)
        return result
# Or
class Solution:
    def inorderTraversal(self, root: Optional[ListNode]) -> List[int]:
        if not root:
            return []
        left = self.inorderTraversal(root.left)
        right = self.inorderTraversal(root.right)
        return left + [root.val] + right

102. Binary Tree Level Order Traversal

class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
    
class Solution:
    def levelOrder(self, root: Optional[ListNode]) -> List[List[int]]:
        if not root:
            return []
        queue = deque([root])
        result = []
        while queue:
            length_of_level = len(queue)
            level = []
            for _ in range(length_of_level):
                node = queue.popleft()
                level.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            result.append(level)
        return result

226. Invert Binary Tree

from typing import List, Optional
from collections import deque
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
class Solution:
    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return None # Al
        queue = deque([root])
        while queue:
            node = queue.popleft() 
            node.left, node.right = node.right, node.left
            # Using parallel assignment in python 
            # temp = node.left
            # node.left = node.right
            # node.right = temp
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        return root

101. Symmetric Tree

len(nums) is even:

  • len(nums) // 2 returns the left middle index.
  • math.ceil(nums / 2) returns the right middle index.

len(nums) is odd:

  • both return the middle one

queue could also store a tuple (node.left, node.right)

from typing import List, Optional
from collections import deque
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
        
class Solution:
    def isSymmetric(self, root: Optional[ListNode]) -> bool:
        if not root:
            return True
        queue = deque([(root.left, root.right)])
        while queue:
            left, right = queue.popleft()
            if not left and not right:
                continue
            if not left or not right or left.val != right.val:
                return False
            queue.append((left.left, right.right)) 
            # compare whether the whole tree is symmetric
            # queue.append((left.left, left.right)): compare the left branch of left is symmetric or not
            queue.append((right.left, left.right))
        return True

104. Maximum Depth of Binary Tree

from collections import deque
from typing import Optional
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
class Solution:
    def maxDepth(self, root: Optional[ListNode]) -> int:
        if not root:
            return 0
        queue = deque([root])
        depth = 0
        while queue:
            depth += 1 
            length = len(queue)
            for _ in range(length):
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return depth
        # In most problems, it doesn't matter whether 
        # you place operations like +1, updates, 
        # or returns at the beginning or end of a loop. 
        # Confused about the initial value of depth? move the position (beginning is more reasonable)

111. Minimum Depth of Binary Tree

leaf node: both node.left and node.right are None

from collections import deque
from typing import Optional
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.right = right
        self.left = left

class Solution:
    def minDepth(self, root: Optional[TreeNode]) -> int:
        queue = deque([root])
        depth = 0
        while queue:
            depth += 1
            length = len(queue)
            level = []
            for _ in range(length):
                node = queue.popleft()
                if node and not node.left and not node.right:
                    return depth
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)

222. Count Complete Tree Nodes

from collections import deque
from typing import Optional
class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.right = right
        self.left = left

class Solution:
    def countNodes(self, root: Optional[TreeNode]) -> int:
        def getDepth(node):
            if not node:
                return 0
            depth = 0
            while node:
                depth += 1
                node = node.left
            if not root:
                return 0
        depth_of_left_branch = getDepth(root.left) # getDepth is the method in countNodes, no need for self when using getDepth in countNodes
        depth_of_right_branch = getDepth(root.right)
        
        if depth_of_left_branch == depth_of_right_branch:
            # the left branch is full
            return 2^depth_of_left_branch - 1 + 1 + self.countNodes(root.right) 
            # + 1 is counting node itself
        else:
            # the right branch is full
            return 2^depth_of_right_branch - 1 + 1 + self.counNodes(root.left)

110. Balanced Binary Tree

class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        """
        Some changes based on getDepth
        def getDepth(node):
            if not node:
                return 0
            left = getDepth(node.left)
            # could be filled for the operations on left branch
            right = getDepth(node.right)
            # could be filled for the operations on right branch

            # could be filled for the operation on current node
            return max(left, right) + 1
        """
        def check(node):
            if not node:
                return 0
            
            left = check(node.left)
            if left == -1:
                return -1

            right = check(node.right)
            if right == -1:
                return -1

            if abs(left - right) > 1:
                return -1
            return max(left, right) + 1
        return check(root) != -1

257. Binary Tree Paths

Process currnt node

  • update the state
    • Add the node’s information into global state or the state passed as a parameter.
      • path problem: state.path.append(node.val)
      • sum problems: state.sum += node.val
      • min/max problems: state.best = max(state.best, node.val)
      • graph visits: state.visited.add(node)
  • Check or Record the result
    • If the current state meets your goal, then save to your results list and optionally return/prune.
      class ListNode:
        def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
      class Solution:
        def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        paths = []
        def _dfs(node, path):
            if not node:
                return
            # ending condition:information to global state
            if not node.left and not node.right:
                paths.append(path)
                return
      
            if node.left:
                _dfs(node.left, path + '->' + str(node.left.val))
                  
            if node.right:
                _dfs(node.right, path + '->' + str(node.right.val))
        if root:
            _dfs(root, str(root.val))
        return paths
      

404. Sum of Left Leaves

class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
class Solution:
    def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
        _sum = 0
        def _dfs(node):
            nonlocal _sum
            if not node:
                return 
            if node.left and not node.left.left and not node.left.right:
            # No matter whether node itself has a right child or not, it does not affect whether node.left is a leaf node.
                _sum += node.left.val
            _dfs(node.left)
            _dfs(node.right)
        _dfs(root)
        return _sum

513. Find Bottom Left Tree Value

class ListNode:
    def __init__(self, val = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
class Solution:
    def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
        queue = deque([root])
        result = []
        while queue:
            length = len(queue)
            level = []
            for _ in range(length):
                node = queue.popleft()
                level.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            result.append(level)
        return result[-1][0]



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • CS252 Final
  • CS252 Mid
  • Backtracking, LeetCode
  • Mock Interview(2), LeetCode
  • NVIDIA, LeetCode