DSA Field Notes
Course map / 06 Trees
NeetCode · DSA for Beginners · Lessons 16-21

Trees Field Notes

Binary trees, binary search trees, insert and remove, the three depth-first orders, breadth-first search, and tree-backed sets and maps. Every diagram uses the same two trees as the NeetCode videos, so you can trace along.

0Why trees exist

Analogy

Think of the folders on your laptop. Home contains Photos and Work. Photos contains Trip. Every folder has exactly one parent, and you can only go down from a folder into its children. A binary tree is the same picture with one rule added: each folder may hold at most two subfolders.

A linked list is a chain: one node, then the next, then the next. A tree is the same idea, nodes and pointers, but each node can point to more than one node below it. That single change gives you a hierarchy (levels of parents and children), and, when the values are kept in order, a structure you can search in O(log n) while still inserting and deleting in O(log n). A sorted array cannot do the second part.

LINKED LIST: next points forward 1 2 3 ∅ BINARY TREE: pointers go down 4 3 6 root leaf leaf
Same parts as a linked list, pointing down instead of forward. A linked list node has one next. A binary tree node has left and right. The arrows only go down, so from the top node you can reach everything, and you can never walk in a circle.

Six lessons, one page. Lesson 16 is vocabulary. Lesson 17 adds the sorted rule (a BST). Lesson 18 changes the tree. Lessons 19 and 20 walk the whole tree in two different orders. Lesson 21 uses a BST to build a sorted set and a sorted map.

1Binary trees

The node: one value, two pointers

Like linked lists, binary trees are built from nodes and pointers. A linked list node points to the next node. A binary tree node has at most two pointers, called the left child and the right child. The first node, the one at the top, is the root. NeetCode draws the pointers going downward, and so does every diagram here.

class ListNode:                 # from the linked list lessons
    def __init__(self, val):
        self.val = val
        self.next = None

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None          # left child, or None
        self.right = None         # right child, or None
4 left val right 3 6 left child (a TreeNode) right child (a TreeNode) each pointer holds a TreeNode object, or None
The only new thing is a second pointer. Everything you learned about next (it holds a node object or None, you must check for None before you use it) applies to left and right. The nodes below a node are called its children; the node above is its parent.

Building a tree by hand

Create the nodes, then attach them. This is the tree NeetCode uses for the BST search lesson: root 2, left child 1, right child 3, and 4 under 3 on the right.

root = TreeNode(2)
root.left = TreeNode(1)
root.right = TreeNode(3)
root.right.right = TreeNode(4)    # 4 is the right child of 3

LeetCode and NeetCode describe a tree as a list, level by level, left to right, with null for a missing child. This tree is [2,1,3,null,null,null,4]: level 1 is 2; level 2 is 1, 3; level 3 is the children of 1 (null, null) then the children of 3 (null, 4). Trailing nulls are dropped. A small helper turns that list into real nodes so you can test locally.

from collections import deque

def build_tree(values):
    """[2,1,3,None,None,None,4] -> root of that tree (LeetCode format)."""
    if not values or values[0] is None:
        return None
    root = TreeNode(values[0])
    queue = deque([root])
    i = 1
    while queue and i < len(values):
        node = queue.popleft()
        if i < len(values) and values[i] is not None:
            node.left = TreeNode(values[i])
            queue.append(node.left)
        i += 1
        if i < len(values) and values[i] is not None:
            node.right = TreeNode(values[i])
            queue.append(node.right)
        i += 1
    return root

Leaves, and why one always exists

A node with no children is a leaf. A node with even one child, left or right, is a non-leaf node. Unlike a linked list, tree pointers go only one way, down, and cycles are not allowed. Mathematically a binary tree is a connected, undirected graph with no cycles: every node is reachable, and there is no way to walk back to where you started. Because there are no cycles, walking down must end somewhere, so a leaf node is always guaranteed to exist.

The words interviewers use

Lesson 16 is mostly definitions, but every tree question is asked in these words, so they must be automatic.

2 root · depth 1 1 leaf · depth 2 3 depth 2 4 depth 3 5 leaf height = 3 nodes (or 2 edges) edges = n - 1 = 4 2 is an ancestor of every node; 4 and 5 are descendants of 3 and of 2
Height counts down from the root, depth counts up from a node. The height of this tree is 3 if you count nodes on the longest root-to-leaf path, or 2 if you count the edges between them. The depth of node 4 is 3: the nodes above it, including itself. Note that 1 is a leaf on depth 2, so leaves are not only on the last level.
  • Root node: the highest node; it has no parent, and every node is reachable from it.
  • Leaf nodes: nodes with no children. Nodes on the last level are guaranteed to be leaves, but leaves can sit on other levels too (node 1 above).
  • Children: a node's left child and right child.
  • Height: measured from the root down to the lowest leaf. A single-node tree has height 1 if you count nodes, or 0 if you count edges. When counting edges, height = n - 1 where n is the number of nodes on the path from the root to the lowest leaf. Ask which one a problem wants; LeetCode 104 counts nodes.
  • Depth: measured from a node up to the root. The root has depth 1, and depth grows as you go down. Depth of a node = how many nodes are above it, itself included.
  • Ancestor: a node connected to all the nodes below it. The root is an ancestor of every node.
  • Descendant: a child of the node, or a child of some other descendant of the node. Everything below.
  • Number of edges: always n - 1 for n nodes, because every node except the root has exactly one pointer coming into it.
def height_nodes(root):             # count nodes on the longest path
    if not root:
        return 0
    return 1 + max(height_nodes(root.left), height_nodes(root.right))

def height_edges(root):             # count edges: one less than nodes
    if not root:
        return -1                     # so a single node gives 1 + (-1) = 0
    return 1 + max(height_edges(root.left), height_edges(root.right))

def count_nodes(root):
    if not root:
        return 0
    return 1 + count_nodes(root.left) + count_nodes(root.right)

# for [2,1,3,None,None,None,4]:  height_nodes = 3,  height_edges = 2,
#                                 count_nodes = 4,  edges = 4 - 1 = 3
Keep in mind

Every tree function starts with the same line: if not root: return <something small>. That line handles the empty tree, and it also handles every missing child, because root.left of a leaf is None. Forget it and you get AttributeError: 'NoneType' object has no attribute 'left'.

2Binary search trees

Analogy

A dictionary. You open it in the middle. If your word comes earlier in the alphabet, you ignore the whole right half and never look at it again. A BST stores that "left is smaller, right is bigger" rule inside the structure itself, so every step throws away half of what is left.

Binary tree vs BST

A binary search tree (BST) is a binary tree with a sorted property: every node in the left subtree is smaller than the root, and every node in the right subtree is greater than the root. The property is recursive: it applies at every node, not only at the top. Node 6 below has 5 on its left and 7 on its right, for the same reason. A BST is the tree version of a sorted array.

4 3 2 6 5 7 everything here < 4 everything here > 4 and the same rule holds again inside each subtree: 2 < 3, and 5 < 6 < 7
Everything left of a node is smaller, everything right is bigger, at every node. "Everything" means the whole subtree, not only the direct child. That is what lets a search skip an entire subtree: if the target is bigger than 4, none of 3 or 2 can be the answer.

Why a BST instead of a sorted array?

Both give you search in O(log n). The difference is what happens when the data changes. Inserting into a sorted array means shifting everything after the new spot, O(n). Inserting into a balanced BST is a walk down the tree plus one pointer change, O(log n). Same for deletion. So BSTs are often preferred when you must search and keep adding and removing.

BST search

Trees are recursive (a subtree is a smaller tree), so we search with recursion. The tree is [2,1,3,null,null,null,4] and the target is 3. Same idea as binary search: target smaller than the current node, go left; target bigger, go right. Reach the target node: return True. Reach None: return False.

def search(root, target):
    if not root:
        return False

    if target > root.val:
        return search(root.right, target)
    elif target < root.val:
        return search(root.left, target)
    else:
        return True
  1. None node: return False. The target is not in this subtree. Base case.
  2. Target greater than the current value: search the right subtree.
  3. Target smaller than the current value: search the left subtree.
  4. Target equals the current value: return True. Base case.

In steps 2 and 3 the recursive call's return value is the current call's return value. There is nothing to do after the call returns, so you write return search(...) and let the answer flow straight back up.

2 1 3 4 ① 3 > 2 → go right ② 3 == 3 → True never visited never visited search(root, 3): two comparisons, half the tree skipped
Two calls, and the left half was never touched. At the root, 3 is bigger than 2, so the left subtree is eliminated in one comparison. Orange is the path the search took. Dimmed nodes were never visited.
Callroot.valCompareActionReturns
search(2, 3)23 > 2return search(root.right, 3) - root.right is node 3whatever the next call returns
search(3, 3)33 == 3foundTrue
unwind--True flows back to search(2, 3), which returns itTrue

The same walk without recursion. Interviewers like this version because it uses no call stack.

def search_iter(root, target):
    cur = root
    while cur:
        if target > cur.val:
            cur = cur.right
        elif target < cur.val:
            cur = cur.left
        else:
            return True
    return False

Time: O(log n) if balanced, O(n) if skewed

A tree is balanced when, at every node, the height of the left subtree equals the height of the right subtree, or differs by 1. In a balanced tree each comparison throws away about half of the remaining nodes, exactly the reasoning from binary search and merge sort, so search takes O(log n) steps.

The worst case is a skewed tree, where every node has only one child and the height equals the number of nodes. Then the search walks a straight line: O(n). A skewed tree is a linked list wearing a tree costume.

BALANCED · height 3 · search O(log n) 4 2 1 3 6 5 7 SKEWED · height 7 · search O(n) 1 2 3 4 5 6 7 same 7 values, inserted in sorted order
Same seven values, two very different trees. Insert order decides the shape. Sorted input into a plain BST gives the worst case. Self-balancing trees (AVL, red-black) fix this automatically; they are not in this course, but you should say the phrase "assuming the tree is balanced" whenever you claim O(log n).

3Insert and remove in a BST

The main benefit of a BST over a sorted array is here: insertion and removal in O(log n), assuming the tree is balanced. Both are "search for the spot, then change one or two pointers".

Insertion: walk down, attach a leaf

Traverse to find the right position, then insert while keeping the sorted property. Start with the BST [4] and insert 6. Two results are valid: [4,null,6] (6 as the right child of 4) or [6,4,null] (6 as the new root with 4 on its left). The first one adds 6 as a leaf, which is easier, and it is what the code does. Then insert 5 into [4,null,6]: 5 is bigger than 4, so go right; 5 is smaller than 6, so go left; that spot is empty, so 5 becomes the left child of 6, giving [4,null,6,5,null].

4 start: [4] 4 6 ① insert 6: 6 > 4 → right slot is None → attach 4 6 5 ② insert 5: 5 > 4 → right 5 < 6 → left slot → attach result: [4, None, 6, 5, None] - the tree only ever grows at a None slot
A new value always lands where the walk hits None. Step ① finds the empty right slot of 4. Step ② finds the empty left slot of 6. No existing node moves; the tree only grows at the bottom.
# Insert a new node and return the root of the BST.
def insert(root, val):
    if not root:
        return TreeNode(val)

    if val > root.val:
        root.right = insert(root.right, val)
    elif val < root.val:
        root.left = insert(root.left, val)
    return root
  • Current node is None: return a brand new node holding val. This is the line that adds the node.
  • val greater than the current value: recurse on the right child. Smaller: recurse on the left child.
  • Return the current node after the recursive call. The return value of each recursive call is assigned to root.left or root.right. That assignment is how the tree structure is maintained: the parent re-attaches whatever the child call hands back, which is either the same child or a brand new node.
CallCompareDoesReturns
insert(4, 5)5 > 4root.right = insert(6, 5)node 4 (unchanged root)
insert(6, 5)5 < 6root.left = insert(None, 5)node 6
insert(None, 5)base casecreates TreeNode(5)new node 5
unwind-6.left = node 5, then 4.right = node 6 (same node as before)root 4, now [4,null,6,5]

Time is O(log n) when balanced: we walk until we reach a leaf, so the number of visited nodes is proportional to the height. Equal values are ignored by this code (neither branch fires, the node is returned unchanged), which is what a set wants. NeetCode's video: "6 is greater than the root 4, so it goes in the right subtree. 5 is greater than 4 but less than 6, so it goes in the left subtree of the tree rooted at 6."

Removal: two cases

Removing is harder because the node may have children that must stay in the tree. Count the target's children.

Case 1: the target has 0 or 1 child. Delete node 2, which has no children: the left pointer of 3 now points to None. Delete node 3, which has one child: the left pointer of the root now points to 2 instead of 3. The child moves up into the hole. Nothing else changes.

Case 2: the target has 2 children. Delete node 6. You cannot point 4 at both 5 and 7. Instead, replace 6 with its in-order successor: the left-most node in the right subtree of the target, which is the smallest node among all nodes greater than the target. Here that is 7. Copy 7 into the target node, then remove the original 7 from the right subtree (it is a leaf, so that is case 1). Every node left of the new 7 is smaller than it and every node right of it is bigger, so the tree stays a valid BST.

4 3 2 6 5 7 delete 2 (leaf): 3.left = None 4 2 6 5 7 3 3 gone delete 3 (one child): 4.left = 2 (child moves up) 4 3 2 7 5 7 old 7 gone delete 6 (two children): copy successor 7 up, remove old 7
Zero or one child: re-point the parent. Two children: copy the successor up, then delete the successor. The in-order successor is found by stepping once to the right and then left as far as possible. Using the in-order predecessor (right-most node of the left subtree) also works; NeetCode uses the successor.
# Return the minimum value node of the BST.
def minValueNode(root):
    curr = root
    while curr and curr.left:
        curr = curr.left
    return curr

# Remove a node and return the root of the BST.
def remove(root, val):
    if not root:
        return None

    if val > root.val:
        root.right = remove(root.right, val)
    elif val < root.val:
        root.left = remove(root.left, val)
    else:                                       # found the node to delete
        if not root.left:
            return root.right                   # 0 or 1 child: hand the other side up
        elif not root.right:
            return root.left
        else:                                   # 2 children
            minNode = minValueNode(root.right)   # in-order successor
            root.val = minNode.val               # copy it up
            root.right = remove(root.right, minNode.val)   # delete the copy below
    return root

Read the middle branch slowly. if not root.left: return root.right covers two situations at once: no children at all (then root.right is None, and the parent's pointer becomes None), and only a right child (the parent's pointer becomes that child). The parent re-attaches whatever is returned, the same trick as in insert.

CallBranchDoesReturns
remove(4, 6)6 > 4root.right = remove(6, 6)node 4
remove(6, 6)found, two childrenminValueNode(7) gives 7; root.val = 7; root.right = remove(7, 7)the node, now holding 7
remove(7, 7)found, no left childreturn root.right, which is NoneNone
unwind-node(7).right = None; 4.right = node(7)root 4, now [4,3,7,2,null,5]

Time and space

Time is proportional to the height, essentially a binary search down the tree: O(log n) balanced, O(n) worst case (unbalanced). Space is the recursion: the number of recursive calls on the stack is also proportional to the height, so O(log n) best, O(n) worst.

The line people forget

root.right = remove(root.right, minNode.val). After copying the successor's value up, the successor still exists lower in the tree. If you skip this line the value appears twice and the tree is no longer a set. The recursive call is safe because the successor has no left child, so it is always case 1.

4Depth-first search

Analogy

Exploring a cave with one rule: always take the left tunnel until it dead-ends, then walk back to the last fork and take the right tunnel. You go as deep as possible before you back up. That is depth-first.

DFS is one of the most common interview algorithms. It is used to traverse trees and graphs; here, trees. To traverse an entire tree we must visit every node. DFS picks a direction, say left, and keeps following pointers as far down-left as possible until it hits None. Then it backtracks (returns to the parent) and goes right. Repeat until every node has been visited. "Go as deep as possible before backtracking."

There are three DFS orders, named after when the parent is visited relative to its children: inorder, preorder, postorder. All three are best written with recursion; each can also be written with an explicit stack (section 7). The tree for this lesson is [4,3,6,2,null,5,7]: root 4, left child 3 with left child 2, right child 6 with children 5 and 7.

INORDER left, node, right 4 3 2 6 5 7 1 2 3 4 5 6 prints 2, 3, 4, 5, 6, 7 PREORDER node, left, right 4 3 2 6 5 7 1 2 3 4 5 6 prints 4, 3, 2, 6, 5, 7 POSTORDER left, right, node 4 3 2 6 5 7 1 2 3 4 5 6 prints 2, 3, 5, 7, 6, 4
Same tree, same left-first walk, three answers. The only thing that changes between the three functions is where the print line sits: between the two recursive calls (inorder), before them (preorder), or after them (postorder). The orange numbers are the print order.

Inorder: left subtree, then the node, then right subtree

def inorder(root):
    if not root:
        return
    inorder(root.left)
    print(root.val)
    inorder(root.right)

Visit order: [2, 3, 4, 5, 6, 7], which is sorted. Inorder prints sorted order only if the tree is a BST. All values to the left are smaller, so we hit the base case at the left-most (smallest) node first, print it, then its parent, then the parent's right subtree, and so on. Run the same function on a non-BST with the same shape, like [1,2,3], and you get [2, 1, 3].

StepCall stack (deepest on the right)What happensPrinted so far
1in(4)call inorder(4.left)-
2in(4) > in(3)call inorder(3.left)-
3in(4) > in(3) > in(2)call inorder(2.left)-
4in(4) > in(3) > in(2) > in(None)base case, return-
5in(4) > in(3) > in(2)print 2; call inorder(2.right) = None, returns; in(2) done2
6in(4) > in(3)print 3; call inorder(3.right) = None, returns; in(3) done2 3
7in(4)print 4; call inorder(4.right)2 3 4
8in(4) > in(6) > in(5) > in(None)slide down-left to 5, hit None, return2 3 4
9in(4) > in(6) > in(5)print 5; right is None; in(5) done2 3 4 5
10in(4) > in(6)print 6; call inorder(6.right)2 3 4 5 6
11in(4) > in(6) > in(7)left None; print 7; right None; in(7) done2 3 4 5 6 7
12(empty)in(6) returns, in(4) returns. Done.2 3 4 5 6 7

Preorder: the node first, then left subtree, then right subtree

def preorder(root):
    if not root:
        return
    print(root.val)
    preorder(root.left)
    preorder(root.right)

Visit order: [4, 3, 2, 6, 5, 7]. The root always comes out first. Preorder is the order you would use to copy a tree or serialize it, because you see each parent before its children.

Postorder: left subtree, right subtree, the node last

def postorder(root):
    if not root:
        return
    postorder(root.left)
    postorder(root.right)
    print(root.val)

Visit order: [2, 3, 5, 7, 6, 4]. The root always comes out last. Postorder is the order for "finish the children, then use their results", such as computing a height or deleting a tree from the bottom.

Time and space (all three)

Time O(n): every node is visited exactly once, regardless of the height. Note: an inorder traversal of a BST builds a sorted array, which is the cheap way to get "all values in order" out of a BST. Space O(h) where h is the height, because that is how deep the call stack gets: O(log n) for a balanced tree, O(n) for a skewed one.

Keep in mind

In an interview you usually collect instead of print. Pass a list down, or use a nested helper: def dfs(node): if not node: return; dfs(node.left); out.append(node.val); dfs(node.right). The order rule is identical.

5Breadth-first search

Analogy

Reading a family tree out loud: first the grandparent, then all the parents, then all the grandchildren. You finish one generation before you say a single name from the next one. That is breadth-first.

DFS prioritizes depth. BFS prioritizes breadth: visit all nodes on one level before moving to the next. For trees BFS is also called level-order traversal.

Implementation: a queue, level by level

BFS is written iteratively. We need a data structure that lets us finish a whole level before starting the next. That is a queue, specifically a deque, because it removes from the head and appends to the tail in O(1). Append children to the tail, remove parents from the head, and each level comes out left to right.

from collections import deque

def bfs(root):
    queue = deque()

    if root:
        queue.append(root)

    level = 0
    while len(queue) > 0:
        print("level: ", level)
        for i in range(len(queue)):        # exactly the nodes of this level
            curr = queue.popleft()
            print(curr.val)
            if curr.left:
                queue.append(curr.left)
            if curr.right:
                queue.append(curr.right)
        level += 1
  1. Append the root to the queue (if there is one).
  2. While the queue is not empty: print the level number.
  3. Loop len(queue) times. That number is the size of the current level, and it is read once, before the loop starts, so children appended during the loop are not counted as part of this level.
  4. For each removed node, append its children, left then right.
  5. After the level is done, level += 1.
  6. The queue empties when every node has been visited, and the while loop ends.
4 3 2 6 5 7 level 0 level 1 level 2 queue at the top of each while turn level 0 4 level 1 3 6 level 2 2 5 7 done empty → loop ends popleft one whole level; append its children behind
The queue holds exactly one level at the top of each while turn. While level 1 (3, 6) is being popped, their children are appended behind them, so when the for loop ends the queue holds exactly level 2. Output: level: 0, 4, level: 1, 3, 6, level: 2, 2, 5, 7.
While turnQueue at startlen(queue)Popped (printed)AppendedQueue at end
level 0[4]143, 6[3, 6]
level 1[3, 6]23, then 62 (from 3); 5, 7 (from 6)[2, 5, 7]
level 2[2, 5, 7]32, 5, 7nothing (all leaves)[]
check[]0--while loop exits

Time and space

Time O(n): each node is added to the queue once and removed once. Space O(n): the queue holds an entire level at a time, and in a full tree the last level holds roughly half of all nodes. Compare with DFS, whose stack holds one root-to-leaf path, O(h).

DFS or BFS?

If the question mentions levels, nearest, shortest, left/right side view, or zigzag, think BFS. If it asks for a property built from children (height, sum, is it balanced, does a path exist), think DFS. Both visit every node, O(n); they differ in order and in what they keep in memory.

6BST sets and maps

Analogy

A paper phone book. Each name appears once (a set of names), each name has a number next to it (a map from name to number), and the pages are sorted, so you can flip to "K" without reading "A" to "J".

Sets and maps, like stacks and queues, are interfaces: a list of operations, not a fixed way of storing things. One way to implement them is with a tree, and then insertion, deletion, and search all run in O(log n).

Sets

A phone book with the names Alice, Brad, Collin. A set ensures unique values: adding "Alice" twice keeps one Alice. Implementing it with a BST also keeps the keys sorted (alphabetically here), because inorder traversal gives sorted order. This is a TreeSet.

Maps

A map works on key-value pairs. Built with a tree it is a TreeMap. The phone book maps names to phone numbers, sorted by key: {'Alice': 123, 'Brad': 345, 'Collin': 678}. The key could be the last name; the value can be a number, an object, any type. The only requirement is that keys must be comparable, because the tree needs an order to decide left or right. TreeMap operations run in O(log n).

Brad 345 Alice 123 Collin 678 key (sorted by the BST) · value (anything) inorder walk → Alice:123, Brad:345, Collin:678 (sorted for free)
A TreeMap is the BST from section 2 with a value glued to every key. Search, insert, and remove compare keys exactly like numbers. An inorder walk gives the entries sorted by key for free, which a hash map cannot do.

Here is a small TreeMap written on top of the insert and search you already know. Keys stay unique because equal keys overwrite the value instead of adding a node.

class TreeMap:
    class Node:
        def __init__(self, key, value):
            self.key, self.value = key, value
            self.left = self.right = None

    def __init__(self):
        self.root = None

    def put(self, key, value):
        def ins(node):
            if not node:
                return TreeMap.Node(key, value)
            if key < node.key:
                node.left = ins(node.left)
            elif key > node.key:
                node.right = ins(node.right)
            else:
                node.value = value          # same key: overwrite, keys stay unique
            return node
        self.root = ins(self.root)

    def get(self, key):
        cur = self.root
        while cur:
            if key < cur.key:
                cur = cur.left
            elif key > cur.key:
                cur = cur.right
            else:
                return cur.value
        return None

    def items(self):                        # inorder = sorted by key
        out = []
        def walk(node):
            if not node:
                return
            walk(node.left)
            out.append((node.key, node.value))
            walk(node.right)
        walk(self.root)
        return out

phone = TreeMap()
phone.put("Collin", 678)
phone.put("Alice", 123)
phone.put("Brad", 345)
phone.get("Brad")      # 345
phone.items()          # [('Alice', 123), ('Brad', 345), ('Collin', 678)]

In different languages

Java and C++ ship a TreeMap in the standard library. Python and JavaScript do not; you install one.

# Python: pip install sortedcontainers
from sortedcontainers import SortedDict
treemap = SortedDict({'c': 3, 'a': 1, 'b': 2})
list(treemap.keys())    # ['a', 'b', 'c']  - always sorted

# Java:        TreeMap treeMap = new TreeMap();
# JavaScript:  const TreeMap = require("treemap-js"); let map = new TreeMap();
# C++:         map<string, string> treeMap;

sortedcontainers also gives you SortedList (a sorted list with O(log n) insert, index, and bisect_left) and SortedSet (the TreeSet). Internally they are not trees but lists of lists; the interface and the Big-O are what matter in an interview.

Keep in mind

Python's built-in dict and set are the hash-based alternative: O(1) average for get, put, and remove, but no order. Reach for a tree-based map only when you need "smallest key greater than x", "keys in a range", or sorted iteration. Hashing is the topic of a later lesson; trees are one way to build sets and maps, hashing is the other.

7Moves that solve most tree problems

The lessons teach the structure and the traversals. Interview questions are those traversals with a small twist. Learn these five moves.

Move 1: the recursive template, "combine(left, right)"

Almost every tree function has this shape: handle None, ask both children, combine their two answers with the current node, return one value. Height, node count, sum, maximum path, "is it the same tree", "invert it" all fit.

def solve(root):
    if not root:
        return base            # 0, True, None, ... whatever an empty tree answers
    left = solve(root.left)
    right = solve(root.right)
    return combine(left, right, root)

def max_depth(root):                 # LeetCode 104 (counts nodes)
    if not root:
        return 0
    left = max_depth(root.left)
    right = max_depth(root.right)
    return 1 + max(left, right)

def invert(root):                    # LeetCode 226: swap every left and right
    if not root:
        return None
    root.left, root.right = invert(root.right), invert(root.left)
    return root

def same_tree(p, q):                 # LeetCode 100: two trees at once
    if not p and not q:
        return True
    if not p or not q or p.val != q.val:
        return False
    return same_tree(p.left, q.left) and same_tree(p.right, q.right)
4 3 2 6 5 7 ↑3 ↑2 ↑2 ↑1 ↑1 ↑1 0 0 0 0 0 0 0 max_depth: None → 0; each node returns 1 + max(left, right); the root answers 3
Answers flow upward, one number per node. This is postorder in disguise: a node cannot answer until both children have. Trace it by writing each node's return value next to it, bottom row first.

When the answer is not the same thing the function returns, keep a side variable. Diameter (longest path, in edges) and "is balanced" both return a height upward while recording something else.

def diameter(root):                  # LeetCode 543
    best = 0

    def height(node):
        nonlocal best
        if not node:
            return 0
        l = height(node.left)
        r = height(node.right)
        best = max(best, l + r)      # longest path THROUGH this node, in edges
        return 1 + max(l, r)         # but hand the height upward

    height(root)
    return best

def is_balanced(root):               # LeetCode 110
    def check(node):                 # returns height, or -1 if anything below is unbalanced
        if not node:
            return 0
        l = check(node.left)
        if l == -1:
            return -1
        r = check(node.right)
        if r == -1:
            return -1
        if abs(l - r) > 1:
            return -1
        return 1 + max(l, r)

    return check(root) != -1

Move 2: DFS with an explicit stack

Same order as the recursive version, but you manage the stack yourself. Preorder is the easy one: pop a node, visit it, push right then left so that left is popped first. Inorder needs a "slide down-left, then pop, then step right" loop.

def preorder_iter(root):
    out, stack = [], [root] if root else []
    while stack:
        node = stack.pop()
        out.append(node.val)
        if node.right:                 # push right first so left is popped first
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
    return out                         # [4, 3, 2, 6, 5, 7]

def inorder_iter(root):
    out, stack, cur = [], [], root
    while cur or stack:
        while cur:                     # slide down-left, remembering the path
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()              # nothing further left: visit
        out.append(cur.val)
        cur = cur.right                # then go right
    return out                         # [2, 3, 4, 5, 6, 7]

The inorder stack version is also the tool for kth smallest in a BST: stop after k pops.

def kth_smallest(root, k):           # LeetCode 230
    stack, cur = [], root
    while cur or stack:
        while cur:
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()
        k -= 1
        if k == 0:
            return cur.val
        cur = cur.right

Move 3: BFS with the level-size loop

The lesson's bfs prints. Interview versions return a list per level. Freeze size = len(queue) before the inner loop; that number is the whole trick.

def level_order(root):               # LeetCode 102
    levels = []
    queue = deque([root] if root else [])
    while queue:
        size = len(queue)              # freeze the size: only this level
        level = []
        for _ in range(size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        levels.append(level)
    return levels                      # [[4], [3, 6], [2, 5, 7]]

def right_side_view(root):           # LeetCode 199: last node of every level
    return [level[-1] for level in level_order(root)]

Move 4: validate a BST with min / max bounds

The trap: checking only left.val < node.val < right.val at each node is not enough. In [5,4,7,null,6], node 6 is a valid right child of 4, but it sits in the left subtree of 5, where everything must be below 5. Carry a range down instead: every node must be strictly between low and high, and each step narrows one side.

def is_valid_bst(root, low=float("-inf"), high=float("inf")):   # LeetCode 98
    if not root:
        return True
    if not (low < root.val < high):
        return False
    return (is_valid_bst(root.left, low, root.val) and
            is_valid_bst(root.right, root.val, high))

Move 5: use the BST order to prune

On a BST, do not visit both children. Compare and go one way, like search. Lowest common ancestor is the classic: the first node where the two targets split to different sides (or one of them equals the node) is the answer.

def lca_bst(root, p, q):             # LeetCode 235, p and q are values
    cur = root
    while cur:
        if p < cur.val and q < cur.val:
            cur = cur.left
        elif p > cur.val and q > cur.val:
            cur = cur.right
        else:
            return cur.val               # they split here (or one is here)

8Big-O summary

Say the word "balanced" every time you say O(log n). h is the height: about log n for a balanced tree, up to n for a skewed one.

OperationBalanced BSTSkewed BSTSorted arrayHash map / set
SearchO(log n)O(n)O(log n)O(1)*
InsertO(log n)O(n)O(n)O(1)*
RemoveO(log n)O(n)O(n)O(1)*
Min or maxO(log n)O(n)O(1)O(n)
All values in sorted orderO(n) inorderO(n) inorderO(n)O(n log n) sort first
DFS (any order), timeO(n)O(n)--
DFS space (call stack or explicit stack)O(log n)O(n)--
BFS timeO(n)O(n)--
BFS space (queue = widest level)O(n)O(1)--
Space of the structureO(n)O(n)O(n)O(n)

The hash map star means "average case"; hashing is a later topic. BFS space is O(n) on a balanced tree because the last level of a full tree holds about n/2 nodes; on a skewed tree each level has one node, so the queue never grows, which is the one place a skewed tree wins. DFS space is the opposite: it holds one root-to-leaf path, which is short on a balanced tree and the whole tree on a skewed one.

9Mistakes to watch for

Forgetting the None base case

AttributeError: 'NoneType' object has no attribute 'left'. First line of every recursive tree function: if not root: return .... It covers the empty tree and every missing child at once.

Calling insert or remove and throwing away the result

insert(root, 5) alone does nothing visible when the tree is empty, and remove(root, 4) cannot delete the root. Always write root = insert(root, 5), and inside the function always write root.left = insert(root.left, val). The return value is how the parent re-attaches the subtree.

Validating a BST by comparing only parent and child

[5,4,7,null,6] passes a local check but is not a BST: 6 lives in the left subtree of 5. Carry low and high bounds down the recursion and tighten one side at each step.

Expecting inorder to be sorted on a plain binary tree

Inorder gives sorted output only when the tree is a BST. On [1,2,3] it gives 2, 1, 3. Check what the problem promises before you rely on it.

Mixing height in nodes with height in edges

A single node has height 1 (nodes) or 0 (edges). LeetCode 104 wants nodes; diameter (543) wants edges. Read the example in the problem and match it, then keep one convention through the whole solution.

Losing the level boundary in BFS

Writing while queue: node = queue.popleft(); ... with no inner loop visits nodes in level order but never knows where a level ends. Freeze size = len(queue) and loop exactly that many times. And never use list.pop(0) as the queue; it is O(n) per pop. Use deque.popleft().

Deleting a 2-child node and leaving the successor behind

After root.val = minNode.val the value exists twice. You must also run root.right = remove(root.right, minNode.val). The successor has no left child, so that second removal is always the easy case.

Claiming O(log n) without saying "balanced"

A plain BST built from sorted input is a linked list: every operation is O(n). Say "O(log n) if the tree is balanced, O(n) in the worst case". Interviewers listen for that sentence.

Pushing left before right in iterative preorder

A stack is LIFO. If you want the left child processed first, push it last. Push right, then left.

Interview one-liners

  • "A binary tree node is a linked list node with two pointers, left and right, pointing down; no cycles, so a leaf always exists."
  • "A BST keeps every left descendant smaller and every right descendant bigger, at every node, so search, insert, and delete are O(log n) if the tree is balanced, O(n) if skewed."
  • "To delete a node with two children, I replace it with its in-order successor, the left-most node of the right subtree, then delete that successor."
  • "Inorder, preorder, postorder are the same left-first DFS; only the position of the visit line changes. Inorder on a BST is sorted."
  • "DFS costs O(n) time and O(h) stack; BFS costs O(n) time and O(n) queue, because it holds a whole level."
  • "I validate a BST by passing min and max bounds down, not by comparing a node with its direct children."
  • "A TreeMap is a BST of keys with values attached, O(log n) per operation and sorted iteration; Python's dict is the hash version, O(1) average and unordered. For a tree-like map in Python I use sortedcontainers.SortedDict."

10Practice, in this order

Do these on LeetCode with the notes closed. The first five are the recursive template with different combine steps; do not move on until they feel like one problem.

#ProblemMove it trainsLevel
104Maximum Depth of Binary Treecombine(left, right): 1 + maxEasy
226Invert Binary Treecombine: swap children, return rootEasy
100Same Treerecursion on two trees at onceEasy
572Subtree of Another Treesame_tree called at every nodeEasy
543Diameter of Binary Treereturn height, record best on the sideEasy
110Balanced Binary Treereturn height or -1 sentinelEasy
94Binary Tree Inorder Traversalinorder, recursive then with a stackEasy
700Search in a Binary Search TreeBST search (lesson 17)Easy
701Insert into a Binary Search TreeBST insert, re-attach the return valueMedium
102Binary Tree Level Order TraversalBFS level-size loopMedium
199Binary Tree Right Side ViewBFS, keep the last node per levelMedium
235Lowest Common Ancestor of a BSTprune with the BST orderMedium
230Kth Smallest Element in a BSTinorder with early stopMedium
98Validate Binary Search Treemin / max boundsMedium
450Delete Node in a BSTthree removal cases, in-order successorMedium

Your next step today: LeetCode 104. Write it from memory, then draw the return value next to every node of [4,3,6,2,null,5,7] and check that the root gets 3.

11Self-check

Answer out loud before opening. If you cannot answer in one breath, reread that section.

Q1How is a TreeNode different from a ListNode, and what is the same?

Same: a value plus pointers to other nodes, and "no node" is None. Different: a ListNode has one pointer, next, pointing forward; a TreeNode has two, left and right, pointing down to its children, and the pointers never form a cycle.

Q2In the tree [2,1,3,null,null,null,4], what are the root, the leaves, the height in nodes, and the height in edges?

Root 2. Leaves 1 and 4 (1 is on depth 2, 4 on depth 3). Height 3 counting nodes on the path 2, 3, 4; height 2 counting the edges between them. The tree has 4 nodes and therefore 3 edges.

Q3State the BST property precisely. Why is "left child smaller, right child bigger" not enough?

Every node in the whole left subtree is smaller than the node, and every node in the whole right subtree is bigger, and this holds at every node. Checking only direct children misses cases like [5,4,7,null,6], where 6 is a fine right child of 4 but violates "everything under 5's left is below 5".

Q4Why prefer a BST over a sorted array, when both search in O(log n)?

Insert and delete. A sorted array shifts elements, O(n). A balanced BST walks down and changes a pointer, O(log n).

Q5Trace it: search for 4 in [2,1,3,null,null,null,4]. List every call and its comparison.

search(2, 4): 4 > 2, go right. search(3, 4): 4 > 3, go right. search(4, 4): equal, return True. Three calls; node 1 is never visited. True is passed back through the two outer calls.

Q6In insert, why is the recursive call written as root.left = insert(root.left, val) and not just insert(root.left, val)?

When the walk reaches None, the call returns a brand new node. The only way that node becomes part of the tree is if the parent stores the returned value in its pointer. The assignment re-attaches the subtree every time, unchanged or grown.

Q7Trace it: remove 6 from [4,3,6,2,null,5,7]. What is the tree after each step?

6 has two children. Its in-order successor is the left-most node of its right subtree: 7. Copy: node 6 now holds 7, tree is [4,3,7,2,null,5,7] with a duplicate. Then remove(right subtree, 7): the leaf 7 has no left child, return its right (None). Final: [4,3,7,2,null,5], still a valid BST.

Q8Write inorder, preorder, and postorder for [4,3,6,2,null,5,7] without looking.

Inorder [2,3,4,5,6,7] (sorted, because it is a BST). Preorder [4,3,2,6,5,7] (root first). Postorder [2,3,5,7,6,4] (root last).

Q9What is the time and space complexity of DFS on a tree, and why does space depend on the shape?

Time O(n): each node once. Space O(h): the call stack holds the current root-to-leaf path. Balanced tree, h is about log n. Skewed tree, h is n.

Q10In BFS, what does for i in range(len(queue)) guarantee, and what breaks if you drop it?

len(queue) is evaluated once, before the loop, so the inner loop processes exactly the nodes of the current level even though children are appended during it. Without it you still visit nodes in level order, but you cannot tell where one level ends and the next begins, so per-level answers (level lists, right side view) are impossible.

Q11Trace it: BFS on [4,3,6,2,null,5,7]. Give the queue at the start of each while turn.

Turn 1: [4], pop 4, append 3 and 6. Turn 2: [3, 6], pop 3 (append 2), pop 6 (append 5, 7). Turn 3: [2, 5, 7], pop all three, nothing appended. Queue is empty, loop ends. Printed order 4, 3, 6, 2, 5, 7.

Q12What is a TreeMap, what must be true about its keys, and what is the Python equivalent?

A map (key to value) stored as a BST of keys, so get, put, and remove are O(log n) and iteration is sorted by key. Keys must be comparable so the tree can decide left or right; values can be anything. Python has no built-in TreeMap: use sortedcontainers.SortedDict, or the built-in dict if you only need O(1) average lookups and do not care about order.