Facebook Pixel

545. Boundary of Binary Tree 🔒

Problem Description

You need to find the boundary of a binary tree, which consists of four parts concatenated in order:

  1. The root node
  2. The left boundary (excluding leaves)
  3. All leaf nodes from left to right
  4. The right boundary in reverse order (excluding leaves)

Left Boundary Rules:

  • Start with the root's left child (if it exists)
  • For each node in the left boundary:
    • If it has a left child, the left child is the next boundary node
    • If it has no left child but has a right child, the right child is the next boundary node
    • Stop when reaching a leaf (the leaf is not part of the left boundary)

Right Boundary Rules:

  • Similar to left boundary but mirror image - start with root's right child
  • Prefer right children over left children
  • The rightmost leaf is not included in the right boundary
  • The final right boundary values should be reversed when added to the result

Leaves:

  • Nodes with no children
  • Collected from left to right across the entire tree
  • The root is never considered a leaf

Special Cases:

  • If the root has no left child, the left boundary is empty
  • If the root has no right child, the right boundary is empty
  • If the tree has only one node (the root), return just that node's value

Given the root of a binary tree, return a list containing the values of all nodes on the boundary, following the order: root → left boundary → leaves → reversed right boundary.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure.

DFS

  • Yes: We arrive at DFS as the recommended approach.

Conclusion: The flowchart suggests using DFS (Depth-First Search) for this binary tree problem.

This makes perfect sense for the Boundary of Binary Tree problem because:

  1. We need to traverse down the left boundary following a specific path (always preferring left child, then right child if no left exists)
  2. We need to collect all leaf nodes in left-to-right order, which requires visiting every node in the tree
  3. We need to traverse down the right boundary following a specific path (always preferring right child, then left child if no right exists)
  4. DFS allows us to control the traversal pattern precisely - we can modify our traversal behavior based on whether we're collecting left boundary, leaves, or right boundary nodes

The solution uses DFS with a parameter i to indicate which type of traversal we're performing:

  • i = 0: Left boundary traversal
  • i = 1: Leaf node collection
  • i = 2: Right boundary traversal

This elegant use of DFS with different modes allows us to collect all the boundary nodes in the required order with a single recursive function structure.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that the boundary of a binary tree can be broken down into distinct, non-overlapping parts that we can collect separately and then combine.

Think about walking around the perimeter of a tree drawn on paper - you'd naturally trace along the left edge, across the bottom (the leaves), and up the right edge. This visual metaphor directly translates to our algorithm.

The challenge is that these three parts (left boundary, leaves, right boundary) have different traversal rules:

  • Left boundary: Always go left when possible, otherwise go right, but stop before reaching a leaf
  • Leaves: Visit all nodes and only collect those without children
  • Right boundary: Always go right when possible, otherwise go left, but stop before reaching a leaf

Since each part requires a different traversal strategy, we could write three separate DFS functions. However, we can be more elegant by using a single DFS function with a parameter that controls its behavior. This parameter (i in the solution) acts like a "mode switch" that tells the function which traversal rule to follow.

Why does root.left == root.right check for a leaf node? This is a clever way to check if both children are None without explicitly writing root.left is None and root.right is None. When both are None, they're equal.

The reason we reverse the right boundary at the end is because DFS naturally collects nodes from top to bottom, but for the boundary traversal, we need the right boundary from bottom to top (to maintain the counterclockwise order around the tree).

Finally, we handle the root separately at the beginning because it's always part of the boundary but doesn't belong to any of the three categories we're collecting.

Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

The implementation uses a single DFS function with a mode parameter to handle all three types of boundary traversals. Let's walk through each component:

Main Function Structure:

  1. First, handle the edge case where the tree has only the root node (when root.left == root.right, meaning both children are None)
  2. Initialize the answer list with the root value
  3. Create three separate lists for collecting left boundary, leaves, and right boundary
  4. Make three DFS calls with different modes
  5. Concatenate the results in the correct order

The DFS Function Parameters:

  • nums: The list to store collected node values for the current traversal type
  • root: The current node being processed
  • i: The mode indicator (0 for left boundary, 1 for leaves, 2 for right boundary)

Mode 0 - Left Boundary Traversal: When i == 0, we collect the left boundary:

  • Check if the node is not a leaf using root.left != root.right (non-leaf condition)
  • If it's not a leaf, add its value to nums
  • Prefer going left: if left child exists, recurse on it
  • Otherwise, recurse on the right child
  • This naturally stops at leaves without including them

Mode 1 - Leaf Collection: When i == 1, we collect all leaves:

  • Check if the node is a leaf using root.left == root.right (both children are None)
  • If it's a leaf, add its value to nums
  • Otherwise, recursively traverse both left and right subtrees
  • This performs a complete traversal collecting only leaf nodes in left-to-right order

Mode 2 - Right Boundary Traversal: When i == 2, we collect the right boundary:

  • Similar to left boundary but mirror image
  • Check if not a leaf, then add to nums
  • Prefer going right: if right child exists, recurse on it
  • Otherwise, recurse on the left child

Final Assembly:

ans += left + leaves + right[::-1]

The right boundary is reversed using [::-1] because DFS collects nodes top-to-bottom, but we need them bottom-to-top for the boundary traversal order.

Time Complexity: O(n) where n is the number of nodes, as we visit each node at most twice (once for boundary/leaf detection)

Space Complexity: O(n) for storing the result and O(h) for the recursion stack, where h is the height of the tree

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the boundary traversal works.

Consider this binary tree:

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

Step 1: Add the root

  • Start with ans = [1]

Step 2: Collect left boundary (mode 0)

  • Start at node 2 (root's left child)
  • Node 2 is not a leaf (has children), add it: left = [2]
  • Go to node 2's left child (node 4)
  • Node 4 is a leaf (no children), stop here
  • Result: left = [2]

Step 3: Collect all leaves (mode 1)

  • Traverse entire tree looking for leaves
  • Starting from node 2:
    • Node 4 is a leaf, add it: leaves = [4]
    • Node 5 has children, skip
    • Node 7 is a leaf, add it: leaves = [4, 7]
    • Node 8 is a leaf, add it: leaves = [4, 7, 8]
  • From node 3:
    • Node 6 is a leaf, add it: leaves = [4, 7, 8, 6]
  • Result: leaves = [4, 7, 8, 6]

Step 4: Collect right boundary (mode 2)

  • Start at node 3 (root's right child)
  • Node 3 is not a leaf (has right child), add it: right = [3]
  • Go to node 3's right child (node 6)
  • Node 6 is a leaf, stop here
  • Result: right = [3]

Step 5: Assemble final answer

  • Concatenate: ans = [1] + [2] + [4, 7, 8, 6] + [3][::-1]
  • Since [3][::-1] = [3], we get:
  • Final answer: [1, 2, 4, 7, 8, 6, 3]

Tracing through the DFS calls:

For the left boundary (i=0) starting at node 2:

dfs([2], node=2, i=0)
  - node 2 is not a leaf (has left and right children)
  - Add 2 to nums: [2]
  - Has left child, recurse on node 4
  - dfs([2], node=4, i=0)
    - node 4 is a leaf (no children)
    - Don't add to nums, return

For leaves (i=1) starting at node 2:

dfs([], node=2, i=1)
  - node 2 is not a leaf
  - Recurse on both children
  - dfs([], node=4, i=1)
    - node 4 is a leaf, add 4: [4]
  - dfs([4], node=5, i=1)
    - node 5 is not a leaf
    - Recurse on children...

This example demonstrates how the single DFS function with different modes elegantly handles the three different traversal patterns needed for the complete boundary.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from typing import List, Optional
9
10class Solution:
11    def boundaryOfBinaryTree(self, root: Optional[TreeNode]) -> List[int]:
12        """
13        Find the boundary of a binary tree in anti-clockwise direction.
14        Boundary includes left boundary, leaves, and right boundary in reverse order.
15        """
16      
17        def collect_boundary(boundary_nodes: List[int], node: Optional[TreeNode], boundary_type: int) -> None:
18            """
19            Collect boundary nodes based on type:
20            0 - Left boundary (excluding leaves)
21            1 - All leaf nodes
22            2 - Right boundary (excluding leaves)
23            """
24            if node is None:
25                return
26          
27            # Check if current node is a leaf
28            is_leaf = (node.left is None and node.right is None)
29          
30            if boundary_type == 0:  # Left boundary
31                # Add non-leaf nodes to left boundary
32                if not is_leaf:
33                    boundary_nodes.append(node.val)
34                    # Prefer left child, otherwise take right child
35                    if node.left:
36                        collect_boundary(boundary_nodes, node.left, boundary_type)
37                    else:
38                        collect_boundary(boundary_nodes, node.right, boundary_type)
39                      
40            elif boundary_type == 1:  # Leaf nodes
41                if is_leaf:
42                    # Add leaf nodes
43                    boundary_nodes.append(node.val)
44                else:
45                    # Traverse both subtrees to find leaves
46                    collect_boundary(boundary_nodes, node.left, boundary_type)
47                    collect_boundary(boundary_nodes, node.right, boundary_type)
48                  
49            else:  # boundary_type == 2, Right boundary
50                # Add non-leaf nodes to right boundary
51                if not is_leaf:
52                    boundary_nodes.append(node.val)
53                    # Prefer right child, otherwise take left child
54                    if node.right:
55                        collect_boundary(boundary_nodes, node.right, boundary_type)
56                    else:
57                        collect_boundary(boundary_nodes, node.left, boundary_type)
58      
59        # Start with root node
60        result = [root.val]
61      
62        # If root is a leaf, return just the root
63        if root.left is None and root.right is None:
64            return result
65      
66        # Collect three parts of the boundary
67        left_boundary = []
68        leaf_nodes = []
69        right_boundary = []
70      
71        # Collect left boundary (starting from root's left child)
72        collect_boundary(left_boundary, root.left, 0)
73      
74        # Collect all leaves from entire tree
75        collect_boundary(leaf_nodes, root, 1)
76      
77        # Collect right boundary (starting from root's right child)
78        collect_boundary(right_boundary, root.right, 2)
79      
80        # Combine all parts: root + left boundary + leaves + reversed right boundary
81        result.extend(left_boundary)
82        result.extend(leaf_nodes)
83        result.extend(reversed(right_boundary))
84      
85        return result
86
1class Solution {
2    public List<Integer> boundaryOfBinaryTree(TreeNode root) {
3        List<Integer> result = new ArrayList<>();
4      
5        // Add root value to result
6        result.add(root.val);
7      
8        // If root is a leaf node, return immediately
9        if (root.left == null && root.right == null) {
10            return result;
11        }
12      
13        // Three lists to store different boundary parts
14        List<Integer> leftBoundary = new ArrayList<>();
15        List<Integer> leaves = new ArrayList<>();
16        List<Integer> rightBoundary = new ArrayList<>();
17      
18        // Collect left boundary (excluding leaves)
19        collectBoundary(leftBoundary, root.left, 0);
20      
21        // Collect all leaves
22        collectBoundary(leaves, root, 1);
23      
24        // Collect right boundary (excluding leaves)
25        collectBoundary(rightBoundary, root.right, 2);
26      
27        // Add all parts to result in order
28        result.addAll(leftBoundary);
29        result.addAll(leaves);
30      
31        // Reverse right boundary for bottom-up traversal
32        Collections.reverse(rightBoundary);
33        result.addAll(rightBoundary);
34      
35        return result;
36    }
37  
38    /**
39     * DFS helper method to collect boundary nodes
40     * @param boundaryList - list to store boundary nodes
41     * @param node - current tree node
42     * @param boundaryType - 0: left boundary, 1: leaves, 2: right boundary
43     */
44    private void collectBoundary(List<Integer> boundaryList, TreeNode node, int boundaryType) {
45        if (node == null) {
46            return;
47        }
48      
49        if (boundaryType == 0) { // Left boundary collection
50            // Only add non-leaf nodes to left boundary
51            if (node.left != null || node.right != null) {
52                boundaryList.add(node.val);
53              
54                // Prefer left child, fall back to right if left doesn't exist
55                if (node.left != null) {
56                    collectBoundary(boundaryList, node.left, boundaryType);
57                } else {
58                    collectBoundary(boundaryList, node.right, boundaryType);
59                }
60            }
61        } else if (boundaryType == 1) { // Leaves collection
62            // Add only leaf nodes
63            if (node.left == null && node.right == null) {
64                boundaryList.add(node.val);
65            } else {
66                // Continue searching for leaves in both subtrees
67                collectBoundary(boundaryList, node.left, boundaryType);
68                collectBoundary(boundaryList, node.right, boundaryType);
69            }
70        } else { // Right boundary collection (boundaryType == 2)
71            // Only add non-leaf nodes to right boundary
72            if (node.left != null || node.right != null) {
73                boundaryList.add(node.val);
74              
75                // Prefer right child, fall back to left if right doesn't exist
76                if (node.right != null) {
77                    collectBoundary(boundaryList, node.right, boundaryType);
78                } else {
79                    collectBoundary(boundaryList, node.left, boundaryType);
80                }
81            }
82        }
83    }
84}
85
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    vector<int> boundaryOfBinaryTree(TreeNode* root) {
15        // Lambda function to traverse different parts of the tree boundary
16        // mode: 0 = left boundary, 1 = leaves, 2 = right boundary
17        auto traverseBoundary = [&](this auto&& traverseBoundary, 
18                                    vector<int>& result, 
19                                    TreeNode* node, 
20                                    int mode) -> void {
21            if (!node) {
22                return;
23            }
24          
25            if (mode == 0) { // Collecting left boundary (excluding leaves)
26                // Check if current node is not a leaf
27                if (node->left != node->right) { // Not a leaf (has at least one child)
28                    result.push_back(node->val);
29                    // Prefer left child for left boundary, otherwise take right
30                    if (node->left) {
31                        traverseBoundary(result, node->left, mode);
32                    } else {
33                        traverseBoundary(result, node->right, mode);
34                    }
35                }
36            } 
37            else if (mode == 1) { // Collecting all leaves
38                // Check if current node is a leaf
39                if (node->left == node->right) { // Both are nullptr, so it's a leaf
40                    result.push_back(node->val);
41                } else {
42                    // Recursively find leaves in left and right subtrees
43                    traverseBoundary(result, node->left, mode);
44                    traverseBoundary(result, node->right, mode);
45                }
46            } 
47            else { // mode == 2: Collecting right boundary (excluding leaves)
48                // Check if current node is not a leaf
49                if (node->left != node->right) { // Not a leaf (has at least one child)
50                    result.push_back(node->val);
51                    // Prefer right child for right boundary, otherwise take left
52                    if (node->right) {
53                        traverseBoundary(result, node->right, mode);
54                    } else {
55                        traverseBoundary(result, node->left, mode);
56                    }
57                }
58            }
59        };
60      
61        // Initialize result with root value
62        vector<int> boundary = {root->val};
63      
64        // If root is a leaf, return just the root
65        if (root->left == root->right) { // Both nullptr, root is a leaf
66            return boundary;
67        }
68      
69        // Collect boundary components
70        vector<int> leftBoundary;
71        vector<int> leaves;
72        vector<int> rightBoundary;
73      
74        // Traverse left boundary (starting from root's left child)
75        traverseBoundary(leftBoundary, root->left, 0);
76      
77        // Traverse all leaves in the tree
78        traverseBoundary(leaves, root, 1);
79      
80        // Traverse right boundary (starting from root's right child)
81        traverseBoundary(rightBoundary, root->right, 2);
82      
83        // Combine all parts: root + left boundary + leaves + reversed right boundary
84        boundary.insert(boundary.end(), leftBoundary.begin(), leftBoundary.end());
85        boundary.insert(boundary.end(), leaves.begin(), leaves.end());
86        boundary.insert(boundary.end(), rightBoundary.rbegin(), rightBoundary.rend());
87      
88        return boundary;
89    }
90};
91
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Finds the boundary of a binary tree in anti-clockwise direction
17 * The boundary includes left boundary, leaves, and right boundary in order
18 * @param root - The root node of the binary tree
19 * @returns Array of node values representing the boundary
20 */
21function boundaryOfBinaryTree(root: TreeNode | null): number[] {
22    // Initialize result with root value
23    const result: number[] = [root.val];
24  
25    // If root is a leaf node (no children), return just the root
26    if (root.left === root.right) {
27        return result;
28    }
29
30    // Arrays to store different boundary components
31    const leftBoundary: number[] = [];
32    const leafNodes: number[] = [];
33    const rightBoundary: number[] = [];
34
35    /**
36     * DFS helper function to traverse the tree and collect boundary nodes
37     * @param boundary - Array to store the boundary nodes
38     * @param node - Current node being processed
39     * @param boundaryType - Type of boundary: 0 for left, 1 for leaves, 2 for right
40     */
41    const traverseBoundary = function (boundary: number[], node: TreeNode | null, boundaryType: number): void {
42        // Base case: if node is null, return
43        if (!node) {
44            return;
45        }
46      
47        // Process left boundary (excluding leaves)
48        if (boundaryType === 0) {
49            // Only add non-leaf nodes to left boundary
50            if (node.left !== node.right) {
51                boundary.push(node.val);
52                // Prefer left child, fallback to right if left doesn't exist
53                if (node.left) {
54                    traverseBoundary(boundary, node.left, boundaryType);
55                } else {
56                    traverseBoundary(boundary, node.right, boundaryType);
57                }
58            }
59        } 
60        // Process all leaf nodes
61        else if (boundaryType === 1) {
62            // Check if current node is a leaf
63            if (node.left === node.right) {
64                boundary.push(node.val);
65            } else {
66                // Continue searching for leaves in both subtrees
67                traverseBoundary(boundary, node.left, boundaryType);
68                traverseBoundary(boundary, node.right, boundaryType);
69            }
70        } 
71        // Process right boundary (excluding leaves)
72        else {
73            // Only add non-leaf nodes to right boundary
74            if (node.left !== node.right) {
75                boundary.push(node.val);
76                // Prefer right child, fallback to left if right doesn't exist
77                if (node.right) {
78                    traverseBoundary(boundary, node.right, boundaryType);
79                } else {
80                    traverseBoundary(boundary, node.left, boundaryType);
81                }
82            }
83        }
84    };
85
86    // Collect left boundary starting from root's left child
87    traverseBoundary(leftBoundary, root.left, 0);
88  
89    // Collect all leaf nodes from entire tree
90    traverseBoundary(leafNodes, root, 1);
91  
92    // Collect right boundary starting from root's right child
93    traverseBoundary(rightBoundary, root.right, 2);
94
95    // Combine all boundaries: root + left + leaves + reversed right
96    // Right boundary is reversed to maintain anti-clockwise order
97    return result.concat(leftBoundary).concat(leafNodes).concat(rightBoundary.reverse());
98}
99

Time and Space Complexity

Time Complexity: O(n)

The algorithm traverses the binary tree to collect boundary nodes in three parts: left boundary, leaves, and right boundary. Each node in the tree is visited at most once during the traversal:

  • The left boundary traversal visits nodes along the leftmost path, which is at most O(h) where h is the height of the tree
  • The leaves traversal performs a complete DFS of the entire tree, visiting all n nodes exactly once
  • The right boundary traversal visits nodes along the rightmost path, which is at most O(h)

Since the leaves traversal dominates with O(n) operations and each node is processed in constant time, the overall time complexity is O(n).

Space Complexity: O(n)

The space complexity consists of:

  • The recursion call stack depth, which in the worst case (skewed tree) can be O(n), and in the best case (balanced tree) is O(log n)
  • The storage for the result lists: left, leaves, right, and ans, which together can store up to O(n) elements in the worst case when all nodes are part of the boundary
  • The final answer list ans which contains the boundary nodes

Therefore, the overall space complexity is O(n) where n is the number of nodes in the binary tree.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Including the Root in Leaf Collection

One of the most common mistakes is accidentally including the root node when collecting leaves, especially in edge cases where the root is the only node in the tree.

Problem Example:

# Incorrect leaf collection that might include root
def collect_leaves(node):
    if node.left is None and node.right is None:
        leaves.append(node.val)  # Root gets added if it's a leaf!
    # ... rest of traversal
  
# Then calling:
collect_leaves(root)  # Root might be counted twice!

Solution: Start leaf collection from root's children, not from the root itself:

# Correct approach
if root.left:
    collect_boundary(leaf_nodes, root.left, 1)
if root.right:
    collect_boundary(leaf_nodes, root.right, 1)

2. Including Leaves in Left/Right Boundaries

Another frequent error is adding leaf nodes to the left or right boundary collections. The boundary should stop just before reaching a leaf.

Problem Example:

# Incorrect - adds leaves to boundary
def collect_left_boundary(node):
    if node:
        boundary.append(node.val)  # Adds ALL nodes including leaves
        if node.left:
            collect_left_boundary(node.left)
        elif node.right:
            collect_left_boundary(node.right)

Solution: Always check if a node is a leaf before adding it to the boundary:

# Correct - excludes leaves from boundary
is_leaf = (node.left is None and node.right is None)
if not is_leaf:
    boundary_nodes.append(node.val)
    # Continue traversal...

3. Forgetting to Reverse the Right Boundary

The right boundary must be added in reverse order (bottom-up), but DFS naturally collects nodes top-down.

Problem Example:

# Incorrect - adds right boundary in wrong order
result.extend(right_boundary)  # Top-down order, should be bottom-up!

Solution: Reverse the right boundary before adding:

# Correct
result.extend(reversed(right_boundary))
# Or
result.extend(right_boundary[::-1])

4. Incorrect Traversal Priority for Boundaries

Left boundary should prefer left children, right boundary should prefer right children. Mixing these up leads to incorrect boundaries.

Problem Example:

# Incorrect left boundary - wrong child preference
if node.right:  # Should check left first!
    collect_boundary(node.right, 0)
elif node.left:
    collect_boundary(node.left, 0)

Solution: Ensure correct child preference for each boundary type:

# Left boundary - prefer left child
if node.left:
    collect_boundary(boundary_nodes, node.left, 0)
else:
    collect_boundary(boundary_nodes, node.right, 0)

# Right boundary - prefer right child  
if node.right:
    collect_boundary(boundary_nodes, node.right, 2)
else:
    collect_boundary(boundary_nodes, node.left, 2)

5. Not Handling Single-Node Tree Correctly

When the tree has only a root node, it should be returned as-is without being processed as a leaf.

Problem Example:

# Incorrect - might process root twice
result = [root.val]
# ... collect boundaries and leaves including root
# Root appears twice in result!

Solution: Check for single-node tree explicitly:

# Correct
if root.left is None and root.right is None:
    return [root.val]  # Early return for single node
# Continue with normal processing...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More