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:
- The root node
- The left boundary (excluding leaves)
- All leaf nodes from left to right
- 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:
- We need to traverse down the left boundary following a specific path (always preferring left child, then right child if no left exists)
- We need to collect all leaf nodes in left-to-right order, which requires visiting every node in the tree
- We need to traverse down the right boundary following a specific path (always preferring right child, then left child if no right exists)
- 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 traversali = 1
: Leaf node collectioni = 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.
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:
- First, handle the edge case where the tree has only the root node (when
root.left == root.right
, meaning both children areNone
) - Initialize the answer list with the root value
- Create three separate lists for collecting left boundary, leaves, and right boundary
- Make three DFS calls with different modes
- Concatenate the results in the correct order
The DFS Function Parameters:
nums
: The list to store collected node values for the current traversal typeroot
: The current node being processedi
: 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 areNone
) - 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 EvaluatorExample 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]
- Node 4 is a leaf, add it:
- From node 3:
- Node 6 is a leaf, add it:
leaves = [4, 7, 8, 6]
- Node 6 is a leaf, add it:
- 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)
whereh
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) isO(log n)
- The storage for the result lists:
left
,leaves
,right
, andans
, which together can store up toO(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...
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!