Start learning free

LESSON 421 OF 543 · BINARY TREES

Same Tree in Rust: Recursion on a Pair of Options in O(n)

Practice O(n) Read the full explanation

Given the roots of two binary trees, p and q, return true if they are the same tree, and false otherwise.

Two binary trees are the same when they have exactly the same shape and every node holds the same value as the node in the same place in the other tree. Two empty trees are the same.

The trees below are written in level order (top to bottom, left to right), with None for a missing child.

 

Example 1:

     p          q
     1          1
    / \        / \
   2   3      2   3

Input: p = [1,2,3], q = [1,2,3]
Output: true
Explanation: Both trees have the root 1, with 2 as its left child and 3 as its right child.

Example 2:

     p          q
     1          1
    /            \
   2              2

Input: p = [1,2], q = [1,None,2]
Output: false
Explanation: The trees hold the same values, but the 2 is the left child of the root in p and the right child in q.

Example 3:

     p          q
     1          1
    / \        / \
   2   1      1   2

Input: p = [1,2,1], q = [1,1,2]
Output: false
Explanation: The shapes match, but the left children hold different values: 2 in p and 1 in q.

 

Constraints:

  • The number of nodes in each tree is in the range [0, 104].
  • Each node value is between -104 and 104.
  • Each tree is at most 400 levels deep.

Note:

Your algorithm should run in O(n) time, where n is the number of nodes, comparing each pair of nodes at most once.

STEP 1 OF 6
    Ctrl + Enter to check

    RUST · BINARY TREES

    Same Tree in Rust, explained

    Problem overview

    The function is_same_tree receives the roots of two binary trees, p and q, each an Option<Box<TreeNode>>, and returns true if they are the same tree. Two trees are the same when they have exactly the same shape and every node holds the same value as the node in the same place in the other tree. Two empty trees are the same. The lesson provides TreeNode, with an i32 field val and the fields left and right, both Option<Box<TreeNode>>.

    The trees [1, 2, 3] and [1, 2, 3] are the same. The trees [1, 2] and [1, None, 2] hold the same values but differ in shape: the 2 is a left child in the first and a right child in the second. The trees [1, 2, 1] and [1, 1, 2] have the same shape and different values in the left child.

    Approach: compare the roots, then the subtrees

    Two trees are the same when their roots hold the same value, their left subtrees are the same, and their right subtrees are the same. The last two parts are the same question about smaller trees, so the function can call itself on them. The recursion stops when it reaches empty trees.

    At each step there are only a few situations:

    • Both trees are empty: they are the same.
    • Both have a node: the values must match, and then both pairs of subtrees must be the same.
    • Exactly one is empty: the shapes differ, so they are not the same.

    Rust expresses this directly as a match on the pair (p, q), one arm per situation. For [1, 2] and [1, None, 2], the function compares the roots 1 and 1, then looks at the left subtrees. One is the node 2, the other is empty, so the answer is false.

    The left subtrees are checked first, so the function stops at that mismatch without looking at the right ones. For [1, 2, 1] and [1, 1, 2], a run that prints each comparison shows:

    compare 1 and 1
    compare 2 and 1
    

    The roots match, the left children don't, and the function returns false.

    Step by step

    1. Match on the pair. The body is a single match (p, q), so both trees are examined together. It has no semicolon after its closing brace, so its value is what the function returns.
    2. Both empty. The arm (None, None) => true is the base case. It also ends the recursion when both trees step past a leaf at the same time.
    3. Both present. The arm (Some(p), Some(q)) binds the two boxes, again named p and q. Inside the arm, p.val and p.left work directly.
    4. Compare the values. If p.val != q.val, return false at once. An early exit needs return, since it isn't the arm's last expression.
    5. Compare the subtrees. The arm ends with two recursive calls, one on p.left and q.left and one on p.right and q.right, joined with &&. With no semicolon, that is the arm's value.
    6. Everything else. The last arm, _ => false, covers exactly one empty tree. Rust requires a match to cover every case, and this arm completes it.

    The solution in Rust

    impl Solution {
        fn is_same_tree(p: Option<Box<TreeNode>>, q: Option<Box<TreeNode>>) -> bool {
            match (p, q) {
                (None, None) => true,
                (Some(p), Some(q)) => {
                    if p.val != q.val {
                        return false;
                    }
                    Self::is_same_tree(p.left, q.left) && Self::is_same_tree(p.right, q.right)
                }
                _ => false,
            }
        }
    }
    

    Rust details worth knowing:

    • Matching on a tuple moves the trees. match (p, q) builds the pair from the two arguments, which the function owns. The patterns then take the boxes out of the pair.
    • Arm order matters. Rust tries the arms from top to bottom. By the time _ is reached, the cases (None, None) and (Some, Some) are gone, which leaves (Some, None) and (None, Some).
    • Shadowing. In (Some(p), Some(q)), the new p and q are boxed nodes, and they hide the parameters. The parameters were Options.
    • Passing subtrees by value. p.left is moved into the recursive call. This is fine here, because the function doesn't need the trees afterwards. Since p.val is read before the call, and i32 is copied, nothing is used after being moved.
    • && short-circuits. If the left pair differs, the right pair isn't compared.
    • The last expression. The arms are expressions of type bool, so the whole match is one.

    Complexity

    Time: O(n), where n is the number of nodes in the smaller tree. Each pair of nodes is compared at most once, and the function stops at the first difference.

    Space: O(h), where h is the height of the trees, for the recursion. The lesson limits the depth to 400.

    Edge cases and common mistakes

    • Both trees empty. [] and [] give true.
    • One tree empty. [] and [0] give false. The _ arm handles it, whichever tree is the empty one.
    • Same values, different shape. [1, 2] and [1, None, 2] give false. Comparing the lists of values, ignoring where the missing children are, would call them equal.
    • A tree that extends the other. [1, 2] and [1, 2, 3] give false: the second has a right child that the first doesn't.
    • Forgetting the value check. Comparing only the shapes gives true for [1, 2, 1] and [1, 1, 2].
    • Treating the zero value as "missing". A node can hold 0 and is still a node: [0] and [0] give true.

    Testing

    • [1, 2, 3] and [1, 2, 3] give true.
    • [1, 2] and [1, None, 2] give false.
    • [1, 2, 1] and [1, 1, 2] give false.
    • [] and [] give true.
    • [] and [0] give false.
    • [0] and [0] give true.
    • [1, 2] and [1, 2, 3] give false.
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free