Start learning free

LESSON 329 OF 414 · BINARY TREES

Symmetric Tree in Rust: Recursive Mirror Check

Practice O(n) Read the full explanation

Given the root of a binary tree, check whether it is a mirror of itself (i.e., symmetric around its center).

 

Example 1:

Input: root = [1,2,2,3,4,4,3]
Output: true

Example 2:

Input: root = [1,2,2,None,3,None,3]
Output: false

 

Constraints:

  • The number of nodes in the tree is in the range [1, 1000].
  • Each node value is between -100 and 100.

 

Follow up: Could you solve it both recursively and iteratively?
STEP 1 OF 7
    Ctrl + Enter to check

    RUST · BINARY TREES

    Symmetric Tree in Rust, explained

    Problem overview

    The function is_symmetric receives the root of a binary tree, as an Option<Box<TreeNode>>, and returns true when the tree is a mirror image of itself around its center. The tree has between 1 and 1,000 nodes and values from -100 to 100.

    The tree [1, 2, 2, 3, 4, 4, 3] is symmetric: the left subtree (2 with children 3 and 4) is the mirror of the right subtree (2 with children 4 and 3). The tree [1, 2, 2, null, 3, null, 3] is not. Both 2s have a single child 3, but on the same side, so the shapes are not mirrored.

    Approach: compare two subtrees that should be mirrors

    Asking whether a whole tree is symmetric is the same as asking whether its left and right subtrees mirror each other. So the real task is a function that takes two subtrees, left and right, and decides whether one is the mirror of the other. Two subtrees mirror each other when:

    • both are empty; or
    • both exist, their root values are equal, and the outer pair mirrors (the left child of the first against the right child of the second) and the inner pair mirrors (the right child of the first against the left child of the second).

    If exactly one of them is empty, they cannot mirror each other.

    Comparing "left with left" would test whether two subtrees are identical, which is a different question. A mirror flips the sides, which is why the outer and inner pairs cross over.

    The check visits each node once, so it is much cheaper than building a mirrored copy of the tree and comparing it afterwards.

    Step by step

    1. Decide the entry point. In is_symmetric, match on root. An empty tree (None) is symmetric, so that arm gives true. Otherwise bind the root as node and return Self::dfs(node.left, node.right). The root is passed by value and moved, so it can't be given to the helper twice. Comparing its two children covers the same ground.
    2. Declare dfs with two parameters, left and right, both Option<Box<TreeNode>>, returning bool. Its body is one match on the pair (left, right), so both values are inspected together.
    3. Two empty subtrees: the arm (None, None) => true.
    4. Two present nodes: an arm (Some(l), Some(r)) with a block body, binding the boxed nodes as l and r.
    5. Compare the values: if l.val != r.val, return false at once.
    6. Cross-compare the children: end the arm with Self::dfs(l.left, r.right) && Self::dfs(l.right, r.left), without a semicolon, so it is the arm's value.
    7. Everything else: the last arm _ => false covers the case where exactly one side is empty. Rust requires a match to be exhaustive, and this arm completes it.

    The solution in Rust

    impl Solution {
        fn dfs(left: Option<Box<TreeNode>>, right: Option<Box<TreeNode>>) -> bool {
            match (left, right) {
                (None, None) => true,
                (Some(l), Some(r)) => {
                    if l.val != r.val {
                        return false;
                    }
                    Self::dfs(l.left, r.right) && Self::dfs(l.right, r.left)
                }
                _ => false,
            }
        }
    
        fn is_symmetric(root: Option<Box<TreeNode>>) -> bool {
            match root {
                None => true,
                Some(node) => Self::dfs(node.left, node.right),
            }
        }
    }
    

    Rust details that matter here:

    • Matching on a tuple. match (left, right) builds a tuple that owns both options and then matches the pair in one go. The arm patterns, (None, None), (Some(l), Some(r)) and _, between them cover all four combinations of present and missing.
    • Moving out of boxes. In the second arm, l and r are Box<TreeNode> values. Their fields can be moved out one by one: l.left and r.right go into one recursive call, and l.right and r.left into the other. The tree is consumed as it is checked, which is fine because the function owns it. l.val and r.val are Copy integers, and they are read before any field is moved.
    • Short-circuit &&. The second recursive call is evaluated only if the first returned true. A mismatch on the outer pair stops the whole check early.
    • Early return. The return false inside the arm leaves the function immediately. The final expression has no semicolon, so it is the value of the arm, and so of the whole match, and of the function.
    • Why the helper isn't called with the root twice. An Option<Box<TreeNode>> is moved when it is passed by value, so Self::dfs(root, root) would not compile. Passing the root's two children works, and it checks the same condition.

    Complexity

    Time: O(n). Each node is paired with exactly one other node, so every node is looked at once, and the recursion ends at the first mismatch if there is one.

    Space: O(h). The recursion is as deep as the tree is tall, h. That is about log n for a balanced tree and up to n for a tree shaped like a chain.

    Edge cases and common mistakes

    • Single node. [1] is symmetric. Both children are None, so dfs returns true.
    • Same values, different shape. [1, 2, 2, null, 3, null, 3] returns false. The left 2 has a right child, the right 2 has a right child as well, and the cross comparison pairs a present node with a missing one.
    • Same shape, different values. [1, 2, 3] returns false because 2 and 3 differ. [1, 2, 2, 3, 4, 5, 3] returns false because the inner pair 4 and 5 differs.
    • One-sided tree. [1, 2] returns false: the left child exists and the right one doesn't, which hits the _ => false arm.
    • Comparing the wrong pairs. Using dfs(l.left, r.left) and dfs(l.right, r.right) checks for identical subtrees. For [1, 2, 2, 3, null, 3, null] that would answer true, but the correct answer is false, because a mirror image of a left child must be a right child.
    • Forgetting the value check. Without it, a tree such as [1, 2, 3] would pass as symmetric, since the structure matches.

    Testing

    • [1, 2, 2, 3, 4, 4, 3] gives true.
    • [1, 2, 2, null, 3, null, 3] gives false.
    • [1, 2, 2, 3, null, null, 3] gives true.
    • [1] gives true.
    • [1, 2] gives false.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free