Skip to content
Commitwise
Menu
Coding Interview

Trees: DFS and BFS

Traverse binary trees depth-first with recursion and stacks, and breadth-first with queues, with Maximum Depth, Validate BST, Level Order Traversal and Lowest Common Ancestor in Swift, Kotlin, Java and TypeScript.

intermediate

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

Folders inside folders, comments with replies, a view hierarchy, a JSON document, a company org chart: all trees. And a big share of coding interview questions are about binary trees, because they test recursion, one of the hardest skills to fake.

Almost every tree problem is solved by one of two ways of visiting the nodes: depth-first (go as deep as possible, then back up) or breadth-first (level by level). Learn both well and most tree problems become “which one, and what do I do at each node?”

New to some of these? Open what you need, or skip ahead.

Basics:What is recursion?

A recursive function calls itself on a smaller version of the problem, until it reaches a case small enough to answer directly (the base case):

countdown(3) → prints 3, calls countdown(2) → prints 2, calls countdown(1) → prints 1, calls countdown(0) → stops

Two rules keep it correct: always have a base case, and make sure every call gets closer to it. Each call waits on the call stack until the smaller call finishes, so very deep recursion (hundreds of thousands of levels) can run out of stack space.

Basics:What are classes and objects?

A class is a blueprint; an object (or instance) is one thing built from it. A User class says every user has a name and an email; User(name: "Ana", email: "ana@example.com") creates one specific user.

  • Properties (fields) are the data each object holds.
  • Methods are functions that belong to the class and work with that data.
  • Swift also has struct, Kotlin has data class and Java has record: shorter ways to define types that mostly hold data. In TypeScript, plain objects described by an interface or type often play that role.
Basics:What do null and nil mean?

null (Kotlin, Java, TypeScript, which also has undefined) and nil (Swift) mean “no value here”: a user without a middle name, a search with no result. Using a missing value as if it were there is the classic crash: NullPointerException on the JVM, “unexpectedly found nil” in Swift.

Swift and Kotlin mark types that may be empty with ? (String?), TypeScript with | null, and they make you handle the empty case. The Null Safety tutorial covers this in detail.

Basics:What does serializing mean?

Serializing turns an object in memory into something you can store or send: a JSON string, bytes in a file, a value in Redis. Deserializing turns it back into an object.

Both sides must agree on the format. If the code that reads data was changed (a field renamed, a class restructured) while old data is still stored somewhere, deserializing can fail, which is why formats like JSON with clear field names are preferred over storing raw objects.

Part Not started

Depth first, breadth first

Imagine exploring a building with many floors of rooms, where each room has doors to two smaller rooms:

  • Depth-first (DFS): walk through one door, then the next door in that room, and keep going until you hit a dead end. Then walk back and try the other door. You go deep first.
  • Breadth-first (BFS): visit every room on the first floor, then every room on the second floor, and so on. You go wide first, one level at a time.

In code, DFS is usually a recursive function (or a stack), and BFS uses a queue. A binary tree is a tree where each node has at most two children, left and right.

DFS visits a subtree completely before its siblings (pre-, in- or post-order depending on when the node itself is processed) and uses O(h) space for the recursion or stack, where h is the height; BFS visits by level using a queue and O(w) space, where w is the maximum width. Most tree problems are DFS with the right return value (what each subtree reports to its parent) or the right parameters (what each node receives from above, like bounds); BFS wins for anything per-level or shortest-path-shaped.

Senior concerns are the recursion depth on skewed trees, choosing traversal order deliberately (in-order on BSTs yields sorted output), and problems where information flows both down and up, like Lowest Common Ancestor. All in the Detailed version.

final class TreeNode {
    var value: Int
    var left: TreeNode?
    var right: TreeNode?
    init(_ value: Int, _ left: TreeNode? = nil, _ right: TreeNode? = nil) {
        self.value = value; self.left = left; self.right = right
    }
}

// DFS: recursion goes deep first
func sum(_ node: TreeNode?) -> Int {
    guard let node else { return 0 }
    return node.value + sum(node.left) + sum(node.right)
}

// BFS: a queue visits level by level
func values(byLevel root: TreeNode?) -> [Int] {
    guard let root else { return [] }
    var queue = [root], head = 0, result: [Int] = []
    while head < queue.count {
        let node = queue[head]; head += 1          // dequeue without O(n) removeFirst
        result.append(node.value)
        if let left = node.left { queue.append(left) }
        if let right = node.right { queue.append(right) }
    }
    return result
}
class TreeNode(var value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

// DFS: recursion goes deep first
fun sum(node: TreeNode?): Int =
    if (node == null) 0 else node.value + sum(node.left) + sum(node.right)

// BFS: a queue visits level by level
fun valuesByLevel(root: TreeNode?): List<Int> {
    if (root == null) return emptyList()
    val queue = ArrayDeque(listOf(root))
    val result = mutableListOf<Int>()
    while (queue.isNotEmpty()) {
        val node = queue.removeFirst()
        result += node.value
        node.left?.let(queue::addLast)
        node.right?.let(queue::addLast)
    }
    return result
}
class TreeNode {
    int value;
    TreeNode left, right;
    TreeNode(int value, TreeNode left, TreeNode right) { this.value = value; this.left = left; this.right = right; }
    TreeNode(int value) { this(value, null, null); }
}

// DFS: recursion goes deep first
static int sum(TreeNode node) {
    if (node == null) return 0;
    return node.value + sum(node.left) + sum(node.right);
}

// BFS: a queue visits level by level
static List<Integer> valuesByLevel(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    if (root == null) return result;
    Deque<TreeNode> queue = new ArrayDeque<>(List.of(root));
    while (!queue.isEmpty()) {
        TreeNode node = queue.pollFirst();
        result.add(node.value);
        if (node.left != null) queue.addLast(node.left);
        if (node.right != null) queue.addLast(node.right);
    }
    return result;
}
class TreeNode {
  constructor(
    public value: number,
    public left: TreeNode | null = null,
    public right: TreeNode | null = null,
  ) {}
}

// DFS: recursion goes deep first
function sum(node: TreeNode | null): number {
  if (node === null) return 0;
  return node.value + sum(node.left) + sum(node.right);
}

// BFS: a queue visits level by level
function valuesByLevel(root: TreeNode | null): number[] {
  if (root === null) return [];
  const queue = [root];
  const result: number[] = [];
  for (let head = 0; head < queue.length; head++) {   // a read index instead of shift()
    const node = queue[head];
    result.push(node.value);
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }
  return result;
}

Notice the shape of the recursive DFS: handle the empty tree (the base case), then combine the answers from both children with the current node. Almost every recursive tree function looks like that. The trick is trusting the recursion: assume sum(node.left) returns the right answer for the left subtree, and only think about what to do with it.

Problemeasy

Maximum Depth of Binary Tree

Return the maximum depth of a binary tree: the number of nodes on the longest path from the root down to a leaf. An empty tree has depth 0.

    3
   / \
  9  20
     / \
    15  7        → 3
›

If you knew the depth of the left subtree and of the right subtree, what would the depth of the whole tree be?

›

Depth = 1 + the larger of the two subtree depths, and an empty tree has depth 0. That’s the whole recursion.

SolutionTry it yourself firstTime O(n)Space O(h)
func maxDepth(_ root: TreeNode?) -> Int {
    guard let root else { return 0 }
    return 1 + max(maxDepth(root.left), maxDepth(root.right))
}
fun maxDepth(root: TreeNode?): Int =
    if (root == null) 0 else 1 + maxOf(maxDepth(root.left), maxDepth(root.right))
int maxDepth(TreeNode root) {
    if (root == null) return 0;
    return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
function maxDepth(root: TreeNode | null): number {
  if (root === null) return 0;
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}

Every node is visited once: O(n) time. The recursion is as deep as the tree is tall: O(h) space, which is O(log n) for a balanced tree and O(n) for a tree shaped like a line. BFS solves it too: count the levels.

Choosing DFS or BFS

UseWhy
DFS returning values (heights, sums, “is balanced”, diameter)each subtree’s answer combines into its parent’s
DFS with parameters (BST bounds, path sums, depth so far)information flows down from the root
In-order DFS on a BSTsorted order: k-th smallest, validate, two-sum in a BST
BFSanything per level, minimum depth (stop at the first leaf), shortest path in unweighted graphs

Space differs in an important way: DFS uses O(h) and BFS uses O(w). A balanced tree with a million nodes has h ≈ 20 but a last level of half a million nodes (the puzzle below); a degenerate, list-shaped tree has w = 1 but h = n, which overflows a recursive DFS. Production code on untrusted trees (user-generated nesting, parsed documents) uses an explicit stack or depth limits.

How wide does BFS get?

Predict the output

A perfectly balanced binary tree has 1,048,575 nodes (20 full levels). What is the largest size the BFS queue reaches?

PatternShapeExamples
DFS returning a valuef(node) = combine(node, f(left), f(right))max depth, balanced, diameter, path sum
DFS with parameterspass bounds / path state downvalidate BST, root-to-leaf paths
In-order on BSTleft, node, right = sortedk-th smallest, BST iterator
BFS by levelsize = queue.count, process size nodeslevel order, right side view, min depth
Bottom-up both waysreturn found-info, decide at nodeLCA, diameter, max path sum

Space: DFS O(h) (O(log n) balanced, O(n) skewed), BFS O(w). Gotchas: BST checks against children only, Int.MIN/MAX as sentinels, recursion depth on skewed or user-generated trees, serialization without null markers, removeFirst() on Swift arrays (or shift() in TypeScript) as a queue.

func preorder(_ root: TreeNode?) -> [Int] {
    var stack = [root].compactMap { $0 }, out: [Int] = []
    while let node = stack.popLast() {
        out.append(node.value)
        if let r = node.right { stack.append(r) }   // right first, so left is processed first
        if let l = node.left { stack.append(l) }
    }
    return out
}
fun preorder(root: TreeNode?): List<Int> {
    val stack = ArrayDeque<TreeNode>().apply { root?.let { addLast(it) } }
    val out = mutableListOf<Int>()
    while (stack.isNotEmpty()) {
        val node = stack.removeLast()
        out += node.value
        node.right?.let(stack::addLast)             // right first, so left is processed first
        node.left?.let(stack::addLast)
    }
    return out
}
List<Integer> preorder(TreeNode root) {
    List<Integer> out = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    if (root != null) stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        out.add(node.value);
        if (node.right != null) stack.push(node.right);   // right first, so left is processed first
        if (node.left != null) stack.push(node.left);
    }
    return out;
}
function preorder(root: TreeNode | null): number[] {
  const stack = root ? [root] : [];
  const out: number[] = [];
  while (stack.length > 0) {
    const node = stack.pop()!;
    out.push(node.value);
    if (node.right) stack.push(node.right);   // right first, so left is processed first
    if (node.left) stack.push(node.left);
  }
  return out;
}

Check this part

  1. Which data structure does breadth-first search use?

  2. What is the base case in most recursive tree functions?

  3. DFS uses O(h) space and BFS O(w). Which is smaller for a balanced tree of a million nodes?

  4. A recursive DFS runs on user-generated nesting shaped like a long chain. What is the risk, and the fix?

DFS goes deep first with recursion or a stack and uses O(h) space; BFS goes level by level with a queue and uses O(w). A recursive tree function handles the empty node, then combines both children's answers.

Part Not started

Binary search trees

A binary search treeBinary search treeA binary tree where every value in a node's left subtree is smaller and every value in its right subtree is larger.Open in glossary → (BST) keeps every value in the left subtree smaller than the node, and every value in the right subtree larger. Here’s a validity check that looks right:

Spot the bugA BST check that accepts invalid trees

Tap the line with the bug.

func isValidBST(_ node: TreeNode?) -> Bool {
    guard let node else { return true }
    if let left = node.left, left.value >= node.value { return false }
    if let right = node.right, right.value <= node.value { return false }
    return isValidBST(node.left) && isValidBST(node.right)
}
fun isValidBST(node: TreeNode?): Boolean {
    if (node == null) return true
    node.left?.let { if (it.value >= node.value) return false }
    node.right?.let { if (it.value <= node.value) return false }
    return isValidBST(node.left) && isValidBST(node.right)
}
boolean isValidBST(TreeNode node) {
    if (node == null) return true;
    if (node.left != null && node.left.value >= node.value) return false;
    if (node.right != null && node.right.value <= node.value) return false;
    return isValidBST(node.left) && isValidBST(node.right);
}
function isValidBST(node: TreeNode | null): boolean {
  if (node === null) return true;
  if (node.left && node.left.value >= node.value) return false;
  if (node.right && node.right.value <= node.value) return false;
  return isValidBST(node.left) && isValidBST(node.right);
}

Three depth-first orders

DFS can process a node before its children (pre-order), between them (in-order) or after them (post-order). On a BST, in-order has a special property:

Predict the output

An in-order traversal (left, node, right) of this BST prints what?

//        4
//      /   \
//     2     6
//    / \   / \
//   1   3 5   7
let root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(6, TreeNode(5), TreeNode(7)))

func inOrder(_ node: TreeNode?) {
    guard let node else { return }
    inOrder(node.left)
    print(node.value, terminator: " ")
    inOrder(node.right)
}
inOrder(root)
//        4
//      /   \
//     2     6
//    / \   / \
//   1   3 5   7
val root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(6, TreeNode(5), TreeNode(7)))

fun inOrder(node: TreeNode?) {
    if (node == null) return
    inOrder(node.left)
    print("${node.value} ")
    inOrder(node.right)
}
inOrder(root)
//        4
//      /   \
//     2     6
//    / \   / \
//   1   3 5   7
TreeNode root = new TreeNode(4, new TreeNode(2, new TreeNode(1), new TreeNode(3)),
                                new TreeNode(6, new TreeNode(5), new TreeNode(7)));

static void inOrder(TreeNode node) {
    if (node == null) return;
    inOrder(node.left);
    System.out.print(node.value + " ");
    inOrder(node.right);
}
inOrder(root);
//        4
//      /   \
//     2     6
//    / \   / \
//   1   3 5   7
const root = new TreeNode(4, new TreeNode(2, new TreeNode(1), new TreeNode(3)), new TreeNode(6, new TreeNode(5), new TreeNode(7)));

const out: number[] = [];
function inOrder(node: TreeNode | null) {
  if (node === null) return;
  inOrder(node.left);
  out.push(node.value);
  inOrder(node.right);
}
inOrder(root);
console.log(out.join(" "));

Serializing a tree

Spot the bugTwo different trees, one string

Tap the line with the bug.

func serialize(_ root: TreeNode?) -> String {
    var out: [String] = []
    func visit(_ node: TreeNode?) {
        guard let node else { return }
        out.append(String(node.value))
        visit(node.left)
        visit(node.right)
    }
    visit(root)
    return out.joined(separator: ",")
}
fun serialize(root: TreeNode?): String {
    val out = mutableListOf<String>()
    fun visit(node: TreeNode?) {
        if (node == null) return
        out += node.value.toString()
        visit(node.left)
        visit(node.right)
    }
    visit(root)
    return out.joinToString(",")
}
String serialize(TreeNode root) {
    StringJoiner out = new StringJoiner(",");
    visit(root, out);
    return out.toString();
}

void visit(TreeNode node, StringJoiner out) {
    if (node == null) return;
    out.add(String.valueOf(node.value));
    visit(node.left, out);
    visit(node.right, out);
}
function serialize(root: TreeNode | null): string {
  const out: string[] = [];
  const visit = (node: TreeNode | null) => {
    if (node === null) return;
    out.push(String(node.value));
    visit(node.left);
    visit(node.right);
  };
  visit(root);
  return out.join(",");
}

Deep dives

Deep diveWhy TreeMap is a red-black treeInterview favorite

A plain BST built from sorted input degenerates into a linked list: O(n) lookups. Self-balancing trees fix that by restructuring on insert and delete. A red-black tree colors nodes and keeps two rules (no red node has a red child; every path from the root to a leaf has the same number of black nodes), which bounds the height to at most 2·log₂(n + 1).

Java’s TreeMap and TreeSet are red-black trees, so get, put, floorKey and ceilingKey are O(log n) worst case. AVL trees balance more strictly (faster lookups, slower updates). Swift, Kotlin outside the JVM and JavaScript have no built-in sorted map.

  • Validate a BST by passing the allowed range down; “no limit” must not be a value the tree can contain (Int.MIN_VALUE), so use wider types, optionals or ±infinity.
  • In-order on a BST is sorted: k-th smallest, iterators, two-sum in a BST. Pre-order with null markers serializes a tree unambiguously.

Check this part

  1. Why is checking only node.left < node < node.right not enough for a BST?

  2. What does an in-order traversal of a BST produce?

  3. Why does the bounds-passing BST check use optional bounds instead of Int.min and Int.max?Why does the bounds-passing BST check use Long bounds instead of Int.MIN_VALUE and Int.MAX_VALUE?Why does the bounds-passing BST check use long bounds instead of Integer.MIN_VALUE and Integer.MAX_VALUE?Why does the bounds-passing BST check start with -Infinity and Infinity?

  4. Why must a pre-order serialization include null markers?

In a binary search tree every value in the left subtree is smaller and every value in the right subtree larger, so validating means passing bounds down, and an in-order traversal gives sorted output.

Part Not started

Thinking in levels

Some questions are naturally about levels: “the rightmost node of each level” (what you’d see from the right side), “the average of each level”, “connect each node to its neighbor on the same level”. BFS with one twist answers all of them, step by step:

  1. Put the root in a queue.
  2. While the queue isn’t empty, read its size: that’s exactly how many nodes the current level has.
  3. Take that many nodes out, processing each, and add their children to the queue (they form the next level).
  4. After the inner loop, one level is complete: record whatever the question asks about it.
Exercisemedium

Binary Tree Level Order Traversal

Return the values of a binary tree level by level, from left to right, as a list of lists.

    3
   / \
  9  20
     / \
    15  7        → [[3], [9, 20], [15, 7]]
›

A plain BFS gives [3, 9, 20, 15, 7], but you can’t tell where one level ends. What does the queue’s size tell you at the start of each round?

›

At the start of a level, the queue holds exactly that level’s nodes. Process queue.count nodes into a new list, adding their children for the next round.

SolutionTry it yourself firstTime O(n)Space O(w)
func levelOrder(_ root: TreeNode?) -> [[Int]] {
    guard let root else { return [] }
    var result: [[Int]] = []
    var level = [root]
    while !level.isEmpty {
        result.append(level.map(\.value))
        level = level.flatMap { [$0.left, $0.right].compactMap { $0 } }   // the next level
    }
    return result
}
fun levelOrder(root: TreeNode?): List<List<Int>> {
    if (root == null) return emptyList()
    val result = mutableListOf<List<Int>>()
    val queue = ArrayDeque(listOf(root))
    while (queue.isNotEmpty()) {
        val size = queue.size                       // nodes in this level
        val level = ArrayList<Int>(size)
        repeat(size) {
            val node = queue.removeFirst()
            level += node.value
            node.left?.let(queue::addLast)
            node.right?.let(queue::addLast)
        }
        result += level
    }
    return result
}
List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    Deque<TreeNode> queue = new ArrayDeque<>(List.of(root));
    while (!queue.isEmpty()) {
        int size = queue.size();                    // nodes in this level
        List<Integer> level = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.pollFirst();
            level.add(node.value);
            if (node.left != null) queue.addLast(node.left);
            if (node.right != null) queue.addLast(node.right);
        }
        result.add(level);
    }
    return result;
}
function levelOrder(root: TreeNode | null): number[][] {
  if (root === null) return [];
  const result: number[][] = [];
  let level = [root];
  while (level.length > 0) {
    result.push(level.map((node) => node.value));
    level = level.flatMap((node) => [node.left, node.right].filter((c): c is TreeNode => c !== null));   // the next level
  }
  return result;
}

The Swift and TypeScript versions build each next level as an array instead of a queue: the same idea, written as “the current level’s children”. Either way every node is processed once, O(n) time, and the queue holds at most one or two levels, O(w) where w is the widest level. Variants in one line each: right side view (take each level’s last value), level averages (average each level), zigzag (reverse every other level).

Information flowing both ways

The harder tree problems combine a post-order “what does my subtree contain?” with a decision at the node. Diameter (longest path between any two nodes) returns the height upward and records leftHeight + rightHeight as a candidate at each node. Lowest Common Ancestor (the exercise) returns “found p, q or both” upward, and the first node where both sides report a find is the answer. Naming the return value precisely is most of the solution.

Exercisemedium

Lowest Common Ancestor of a Binary Tree

Given a binary tree (not necessarily a BST) and two nodes p and q in it, return their lowest common ancestor: the deepest node that has both p and q as descendants (a node counts as a descendant of itself).

        3
      /   \
     5     1
    / \   / \
   6   2 0   8
      / \
     7   4

p = 5, q = 1  → 3
p = 5, q = 4  → 5   (5 is an ancestor of 4, and of itself)
›

Ask each subtree a question: “do you contain p or q?” What should a node conclude when both its left and right subtrees say yes?

›

Return the node itself if it’s p or q; otherwise recurse left and right. If both sides return something, this node is the LCA. If only one side does, pass that result up.

SolutionTry it yourself firstTime O(n)Space O(h)
func lowestCommonAncestor(_ root: TreeNode?, _ p: TreeNode, _ q: TreeNode) -> TreeNode? {
    guard let root else { return nil }
    if root === p || root === q { return root }
    let left = lowestCommonAncestor(root.left, p, q)
    let right = lowestCommonAncestor(root.right, p, q)
    if left != nil && right != nil { return root }  // p and q are on different sides
    return left ?? right                            // both on one side, or not found here
}
fun lowestCommonAncestor(root: TreeNode?, p: TreeNode, q: TreeNode): TreeNode? {
    if (root == null || root === p || root === q) return root
    val left = lowestCommonAncestor(root.left, p, q)
    val right = lowestCommonAncestor(root.right, p, q)
    return when {
        left != null && right != null -> root      // p and q are on different sides
        else -> left ?: right                      // both on one side, or not found here
    }
}
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    if (left != null && right != null) return root;  // p and q are on different sides
    return left != null ? left : right;              // both on one side, or not found here
}
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
  if (root === null || root === p || root === q) return root;
  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);
  if (left !== null && right !== null) return root;   // p and q are on different sides
  return left ?? right;                               // both on one side, or not found here
}

Each call returns “the LCA if both are below me, or whichever of p and q I found, or nothing”. The first node where both sides report a find is the deepest one, because the recursion returns bottom-up. If p is an ancestor of q, the search stops at p and returns it, which is correct by the definition. Follow-ups: for a BST, walk from the root and go left or right while both values are on the same side, O(h) with no recursion; with parent pointers, it becomes “intersection of two linked lists”; and if p or q might be missing from the tree, you must track that both were actually found.

  • Per-level BFS: size = queue.count, process size nodes. Right side view, level averages, zigzag and minimum depth are one-line variants.
  • Bottom-up problems: name the return value precisely. LCA returns “the LCA, or whichever of p and q I found, or nothing”; the first node with finds on both sides is the answer. On a BST, walk down in O(h) instead.

Check this part

  1. In level-order BFS, how do you know where one level ends?

  2. How do you get the "right side view" (the last node you'd see on each level)?

  3. In the recursive LCA, when is the current node the answer?

  4. The tree is a BST. How do you find the LCA of p and q?

For per-level questions, read the queue size at the start of each round and process exactly that many nodes. For problems where information flows both ways, name what each subtree returns: LCA reports finds upward and decides where both sides found one.

Key takeaways

  • DFS goes deep first (recursion or a stack); BFS goes level by level (a queue).
  • Recursive tree functions: handle the empty node, then combine both children’s answers with the current node.
  • BST rules apply to whole subtrees: pass bounds down when validating.
  • For per-level questions, read the queue size at the start of each level.
  • Choose by information flow: values up (post-order), constraints down (parameters), levels (BFS), sorted (in-order on BSTs).
  • DFS costs O(h), BFS O(w); skewed or untrusted trees need explicit stacks or depth limits.
  • Name each recursive function’s return value precisely; LCA and diameter follow from it.
  • Serialize with null markers (or explicit structure) and version the format.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.