Start learning free

LESSON 359 OF 445 · BINARY TREES

Flatten Binary Tree to Linked List in Rust: Reverse Preorder DFS

Practice O(n) Read the full explanation

Given the root of a binary tree, flatten the tree into a "linked list":

  • The "linked list" should use the same tree node type (TreeNode), where the right child link points to the next node in the list and the left child link is always None.
  • The "linked list" should be in the same order as a pre-order traversal of the binary tree.

 

Example 1:

Input: root = [1,2,5,3,4,None,6]
Output: [1,None,2,None,3,None,4,None,5,None,6]
Explanation: The root 1 has a left child 2 and a right child 5. Node 2 has the children
3 (left) and 4 (right), and node 5 has only a right child, 6. The pre-order traversal is
1, 2, 3, 4, 5, 6, so after flattening each node's right child is the next one,
1 -> 2 -> 3 -> 4 -> 5 -> 6, and every left child is None.

Example 2:

Input: root = []
Output: []

Example 3:

Input: root = [0]
Output: [0]

 

Constraints:

  • The number of nodes in the tree is in the range [0, 1000].
  • 0 <= TreeNode.val <= 1000
STEP 1 OF 9
    Ctrl + Enter to check

    RUST · BINARY TREES

    Flatten Binary Tree in Rust, explained

    Problem overview

    The function flatten receives root, a mutable reference to an Option<Box<TreeNode>>, and rearranges the tree in place into a "linked list" made of the same TreeNode type. In the result every node's left is None, its right points to the next node of the list, and the nodes are in the same order as a preorder traversal of the original tree: node, then left subtree, then right subtree.

    For the tree [1, 2, 5, 3, 4, null, 6] the preorder is 1, 2, 3, 4, 5, 6. The flattened tree is [1, null, 2, null, 3, null, 4, null, 5, null, 6]: a chain going down the right side. An empty tree stays empty, and the tree [0] stays [0].

    Approach: build the list backwards

    A first idea is to collect the nodes in preorder into a vector and then relink them one after another. That works, but it needs extra space for the vector and a way to take the nodes out of the tree.

    A neater idea uses the order of work. Imagine you already have the flattened list for everything that comes after some node in preorder. Then to flatten that node's part of the tree, you only need to put the node in front of that list. Preorder is node, left, right, so what comes after a node is its left subtree, then its right subtree, then whatever followed the whole subtree.

    So the recursion processes the tree in the opposite order: right subtree first, then left subtree, then the node. The variable last_node always holds the head of the list built so far. After the right subtree has been handled, it holds the right subtree's list. After the left subtree has been handled, the left subtree's nodes sit in front of it, so it holds the left list followed by the right list. Finally the node itself takes that list as its right and becomes the new head.

    On [1, 2, 5, 3, 4, null, 6] the nodes are linked in the order 6, 5, 4, 3, 2, 1, each one placed in front of the list: after 6 the list is 6, after 5 it is 5, 6, after 4 it is 4, 5, 6, and so on until 1, 2, 3, 4, 5, 6.

    Step by step

    1. Create last_node in flatten as a mutable variable starting at None. It holds the list flattened so far, and nothing has been flattened yet. The Solution struct has no fields to keep it in, so the recursion receives it as a mutable reference.
    2. Call the helper Self::dfs(root.take(), &mut last_node). take() moves the whole tree out of the Option, leaving None behind, so dfs can own the nodes and relink them.
    3. Put the result back. When dfs returns, last_node holds the flattened tree. Assigning it to *root hands it back to the caller.
    4. Declare dfs with root: Option<Box<TreeNode>>, which it owns, and last_node: &mut Option<Box<TreeNode>>. It returns nothing.
    5. Handle the empty case. if let Some(mut node) = root skips everything when the subtree is empty. The mut lets the code change the node's children.
    6. Flatten the right subtree first, with node.right.take(), passing last_node on as it is.
    7. Flatten the left subtree the same way with node.left.take(). Taking the left child also leaves node.left as None, which every node of the result needs.
    8. Link the list after the node. Set node.right to last_node.take().
    9. Make the node the head. Store Some(node) in *last_node.

    The solution in Rust

    impl Solution {
        fn dfs(root: Option<Box<TreeNode>>, last_node: &mut Option<Box<TreeNode>>) {
            if let Some(mut node) = root {
                Self::dfs(node.right.take(), last_node);
                Self::dfs(node.left.take(), last_node);
                node.right = last_node.take();
                *last_node = Some(node);
            }
        }
    
        fn flatten(root: &mut Option<Box<TreeNode>>) {
            let mut last_node = None;
            Self::dfs(root.take(), &mut last_node);
            *root = last_node;
        }
    }
    

    Rust details worth noticing:

    • Option::take(). It replaces the value in an Option with None and returns the old value. That is how code moves a child out of a node that is only borrowed, because assigning node.right away without leaving something behind isn't allowed.
    • Ownership flows down. dfs takes root by value, so each call owns its subtree. A node is moved into *last_node in the last line, and the next call moves it back out with take(). Nothing is copied, only the boxes are moved.
    • &mut passed along. last_node is already a mutable reference, so the recursive calls pass it directly. Passing it as it is lends it for the call, and it is usable again afterwards. Writing &mut last_node there is an error: it tries to borrow the parameter itself mutably, and the parameter isn't declared mut.
    • *last_node and *root. The star reaches the Option that the reference points to. Assigning to *root changes the caller's own variable, which is how a function with no return value delivers its result.
    • mut node in the pattern. The binding is mutable so that node.right = ... is allowed. Without mut the compiler rejects the assignment.
    • Associated functions. Neither function takes self, so the recursive calls are written Self::dfs.

    Complexity

    Time: O(n). Each node is visited once, and each visit does a constant number of take and assignment operations.

    Space: O(h), where h is the height of the tree, for the recursion stack. For a balanced tree that is about log n, and for a tree that leans entirely to one side it is n. No other structure is allocated, because the nodes are relinked in place.

    Edge cases and common mistakes

    • An empty tree. root.take() gives None, dfs does nothing, and *root becomes None again.
    • A single node. It is flattened into itself: right becomes None and the node is stored in last_node.
    • A node with only a left child. For [1, 2] the list is 1, 2 on the right side: the left child moves to right, and the old left becomes None.
    • A node with only a right child. For [1, null, 2] the tree is already flat. The right subtree is flattened first, the left one is empty, and the node links to it.
    • Visiting the left subtree before the right. This builds the list in the wrong order, because the list is built from its last node to its first.
    • Forgetting to clear the left link. Using node.left.take() clears it. Passing a clone, or reading the child without taking it, leaves old left children in the result.
    • Very deep trees. The recursion goes as deep as the tree is tall, so a tree shaped like a very long chain can overflow the stack.

    Testing

    • [1, 2, 5, 3, 4, null, 6] gives [1, null, 2, null, 3, null, 4, null, 5, null, 6].
    • [] gives [].
    • [0] gives [0].
    • [1, 2, null, 3] gives [1, null, 2, null, 3].
    • [1, null, 2, 3] gives [1, null, 2, null, 3]: here 3 is the left child of 2.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free