515. Find Largest Value in Each Tree Row
Problem Description
You are given the root of a binary tree. Your task is to find the largest value in each row (level) of the tree and return these values as an array.
The tree is 0-indexed, meaning the root is at level 0, its children are at level 1, and so on.
For example, if you have a binary tree like this:
1 / \ 3 2 / \ \ 5 3 9
The largest values at each level would be:
- Level 0:
1
(only one node) - Level 1:
3
(between 3 and 2) - Level 2:
9
(between 5, 3, and 9)
So the output would be [1, 3, 9]
.
The solution uses a Breadth-First Search (BFS) approach with a queue. Starting from the root, it processes all nodes at each level before moving to the next level. For each level, it tracks the maximum value among all nodes at that level. The algorithm works by:
- Starting with the root node in a queue
- For each level, processing all nodes currently in the queue
- Finding the maximum value among these nodes using
max(x, node.val)
wherex
starts at negative infinity - Adding all children of the current level's nodes to the queue for the next iteration
- Appending the maximum value found to the result array
- Repeating until the queue is empty (all levels processed)
If the tree is empty (root is None
), the function returns an empty array.
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 with nodes (TreeNodes) and edges (parent-child relationships).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure where each node has at most two children and there are no cycles.
DFS
- Yes: We arrive at DFS as a suitable approach for this tree problem.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this tree traversal problem.
While the provided solution uses BFS (level-order traversal with a queue), DFS is equally valid for finding the largest value in each row. With DFS, we would:
- Track the current depth/level as we traverse
- Maintain a list where index corresponds to the tree level
- Update the maximum value at each level as we visit nodes
- Use recursion to traverse left and right subtrees, incrementing the depth
Here's how DFS would work:
def largestValues(self, root: Optional[TreeNode]) -> List[int]:
ans = []
def dfs(node, depth):
if not node:
return
if depth == len(ans):
ans.append(node.val)
else:
ans[depth] = max(ans[depth], node.val)
dfs(node.left, depth + 1)
dfs(node.right, depth + 1)
dfs(root, 0)
return ans
Both DFS and BFS are valid approaches for this level-based tree problem, with BFS being more intuitive for level-order operations and DFS being a natural tree traversal pattern.
Intuition
When we need to find the largest value in each row of a tree, we're essentially looking at a level-by-level analysis problem. The key insight is that we need to process all nodes at the same depth together to compare their values.
Think of the tree as floors in a building - we want to find the maximum value on each floor. To do this systematically, we have two main approaches:
Why BFS feels natural here: Since we're looking for row-wise (level-wise) maximum values, it makes sense to process the tree level by level. BFS naturally groups nodes by their level - we process all nodes at depth 0, then all nodes at depth 1, and so on. This grouping aligns perfectly with our goal of finding the maximum value per level.
With BFS, we use a queue to maintain all nodes at the current level. Before moving to the next level, we:
- Count how many nodes are at the current level (
len(q)
) - Process exactly that many nodes, tracking their maximum value
- While processing, add their children to the queue for the next level
The initialization of x = -inf
ensures that even negative values in the tree are handled correctly, as any actual node value will be greater than negative infinity.
Why DFS also works: Although less intuitive for level-order problems, DFS can solve this by tracking the depth as we traverse. As we visit each node, we know its depth in the tree. We can maintain an array where ans[i]
stores the maximum value seen so far at depth i
. Each time we visit a node at depth d
, we either:
- Initialize
ans[d]
if this is the first node we've seen at this depth - Update
ans[d]
if the current node's value is larger
The beauty of both approaches is that they guarantee we visit every node exactly once, making the time complexity O(n)
where n
is the number of nodes. The BFS approach just happens to group nodes more naturally for this particular problem.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The reference solution uses BFS (Breadth-First Search) to traverse the tree level by level. Let's walk through the implementation step by step:
Data Structures Used:
deque
: A double-ended queue for efficient O(1) operations when adding/removing from both endsans
: A list to store the maximum value found at each level
Algorithm Implementation:
-
Handle Edge Case: First, we check if the root is
None
. If the tree is empty, we return an empty list immediately. -
Initialize BFS: We create a queue
q
usingdeque([root])
and start with the root node in it. The deque allows us to efficientlypopleft()
andappend()
nodes. -
Level-by-Level Processing: The outer
while q:
loop continues as long as there are nodes to process. Each iteration of this loop represents processing one complete level of the tree. -
Track Maximum per Level: For each level, we:
- Initialize
x = -inf
to track the maximum value at this level. Using negative infinity ensures any actual node value will be larger. - Use
for _ in range(len(q)):
to process exactly the number of nodes that were in the queue at the start of this level. This is crucial -len(q)
is evaluated once before the loop starts, giving us the exact count of nodes at the current level.
- Initialize
-
Process Each Node: Within the inner loop:
- Remove a node from the front of the queue with
node = q.popleft()
- Update the maximum value:
x = max(x, node.val)
- Add the node's children to the queue for the next level:
if node.left: q.append(node.left) if node.right: q.append(node.right)
- Remove a node from the front of the queue with
-
Store Level Maximum: After processing all nodes at the current level, append the maximum value found to our result:
ans.append(x)
Why This Works:
- The queue maintains a clear separation between levels. When we start processing a level, the queue contains only nodes from that level.
- As we process these nodes, we add their children (next level nodes) to the back of the queue.
- The
range(len(q))
ensures we process exactly the nodes from the current level before moving to the next.
Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once.
Space Complexity: O(w) where w is the maximum width of the tree, which is the maximum number of nodes at any level. In the worst case (a complete binary tree), this could be O(n/2) = O(n).
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 the BFS solution with this example tree:
1 / \ 3 2 / \ \ 5 3 9
Initial Setup:
root = 1
, which is notNone
, so we proceed- Create
q = deque([1])
(queue contains just the root) - Initialize
ans = []
Level 0 Processing:
- Queue state:
[1]
len(q) = 1
, so we'll process 1 node- Initialize
x = -inf
- Process node 1:
node = q.popleft()
→ node = 1x = max(-inf, 1)
→ x = 1- Add children:
q.append(3)
,q.append(2)
- Queue now:
[3, 2]
- Append x to ans:
ans = [1]
Level 1 Processing:
- Queue state:
[3, 2]
len(q) = 2
, so we'll process 2 nodes- Initialize
x = -inf
- Process node 3:
node = q.popleft()
→ node = 3x = max(-inf, 3)
→ x = 3- Add children:
q.append(5)
,q.append(3)
- Queue now:
[2, 5, 3]
- Process node 2:
node = q.popleft()
→ node = 2x = max(3, 2)
→ x = 3 (stays 3)- Add children: no left child,
q.append(9)
- Queue now:
[5, 3, 9]
- Append x to ans:
ans = [1, 3]
Level 2 Processing:
- Queue state:
[5, 3, 9]
len(q) = 3
, so we'll process 3 nodes- Initialize
x = -inf
- Process node 5:
node = q.popleft()
→ node = 5x = max(-inf, 5)
→ x = 5- No children to add
- Queue now:
[3, 9]
- Process node 3:
node = q.popleft()
→ node = 3x = max(5, 3)
→ x = 5- No children to add
- Queue now:
[9]
- Process node 9:
node = q.popleft()
→ node = 9x = max(5, 9)
→ x = 9- No children to add
- Queue now:
[]
- Append x to ans:
ans = [1, 3, 9]
Final Step:
- Queue is empty, exit while loop
- Return
ans = [1, 3, 9]
The key insight is that len(q)
captures the exact number of nodes at each level before we start processing, ensuring we handle all nodes at one level before moving to the next.
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 collections import deque
9from typing import Optional, List
10from math import inf
11
12class Solution:
13 def largestValues(self, root: Optional[TreeNode]) -> List[int]:
14 """
15 Find the largest value in each row of a binary tree.
16
17 Uses BFS (level-order traversal) to process nodes level by level,
18 tracking the maximum value at each level.
19
20 Args:
21 root: Root node of the binary tree
22
23 Returns:
24 List containing the maximum value from each level of the tree
25 """
26 result = []
27
28 # Handle empty tree case
29 if root is None:
30 return result
31
32 # Initialize queue with root node for BFS
33 queue = deque([root])
34
35 # Process tree level by level
36 while queue:
37 # Track maximum value for current level
38 level_max = -inf
39 # Get number of nodes at current level
40 level_size = len(queue)
41
42 # Process all nodes at current level
43 for _ in range(level_size):
44 # Remove and process next node from queue
45 current_node = queue.popleft()
46 # Update maximum value for this level
47 level_max = max(level_max, current_node.val)
48
49 # Add children to queue for next level processing
50 if current_node.left:
51 queue.append(current_node.left)
52 if current_node.right:
53 queue.append(current_node.right)
54
55 # Add maximum value of current level to result
56 result.append(level_max)
57
58 return result
59
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Finds the largest value in each level of a binary tree.
19 * Uses BFS (Breadth-First Search) to traverse the tree level by level.
20 *
21 * @param root The root node of the binary tree
22 * @return A list containing the maximum value from each level
23 */
24 public List<Integer> largestValues(TreeNode root) {
25 // Initialize result list to store maximum values for each level
26 List<Integer> result = new ArrayList<>();
27
28 // Handle edge case: empty tree
29 if (root == null) {
30 return result;
31 }
32
33 // Initialize queue for BFS traversal
34 Deque<TreeNode> queue = new ArrayDeque<>();
35 queue.offer(root);
36
37 // Process tree level by level
38 while (!queue.isEmpty()) {
39 // Initialize max value for current level with first node's value
40 int maxValueInLevel = queue.peek().val;
41
42 // Get current level size to process all nodes at this level
43 int levelSize = queue.size();
44
45 // Process all nodes in the current level
46 for (int i = 0; i < levelSize; i++) {
47 TreeNode currentNode = queue.poll();
48
49 // Update maximum value for this level
50 maxValueInLevel = Math.max(maxValueInLevel, currentNode.val);
51
52 // Add left child to queue for next level processing
53 if (currentNode.left != null) {
54 queue.offer(currentNode.left);
55 }
56
57 // Add right child to queue for next level processing
58 if (currentNode.right != null) {
59 queue.offer(currentNode.right);
60 }
61 }
62
63 // Add the maximum value of current level to result
64 result.add(maxValueInLevel);
65 }
66
67 return result;
68 }
69}
70
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> largestValues(TreeNode* root) {
15 vector<int> result;
16
17 // Handle empty tree case
18 if (!root) {
19 return result;
20 }
21
22 // Initialize queue for BFS with root node
23 queue<TreeNode*> bfsQueue;
24 bfsQueue.push(root);
25
26 // Process tree level by level
27 while (!bfsQueue.empty()) {
28 int levelSize = bfsQueue.size();
29 int maxValueInLevel = INT_MIN;
30
31 // Process all nodes at current level
32 for (int i = 0; i < levelSize; ++i) {
33 TreeNode* currentNode = bfsQueue.front();
34 bfsQueue.pop();
35
36 // Update maximum value for this level
37 maxValueInLevel = max(maxValueInLevel, currentNode->val);
38
39 // Add children to queue for next level processing
40 if (currentNode->left) {
41 bfsQueue.push(currentNode->left);
42 }
43 if (currentNode->right) {
44 bfsQueue.push(currentNode->right);
45 }
46 }
47
48 // Store the maximum value of current level
49 result.push_back(maxValueInLevel);
50 }
51
52 return result;
53 }
54};
55
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 largest value in each level of a binary tree
17 * @param root - The root node of the binary tree
18 * @returns An array containing the maximum value from each level
19 */
20function largestValues(root: TreeNode | null): number[] {
21 // Initialize result array to store maximum values from each level
22 const result: number[] = [];
23
24 // Handle empty tree case
25 if (!root) {
26 return result;
27 }
28
29 // Initialize queue for BFS traversal with the root node
30 const currentLevelQueue: TreeNode[] = [root];
31
32 // Process tree level by level
33 while (currentLevelQueue.length > 0) {
34 // Queue to store nodes for the next level
35 const nextLevelQueue: TreeNode[] = [];
36 // Track maximum value in current level
37 let maxValueInLevel: number = -Infinity;
38
39 // Process all nodes in the current level
40 for (const node of currentLevelQueue) {
41 // Update maximum value for this level
42 maxValueInLevel = Math.max(maxValueInLevel, node.val);
43
44 // Add left child to next level queue if it exists
45 if (node.left) {
46 nextLevelQueue.push(node.left);
47 }
48
49 // Add right child to next level queue if it exists
50 if (node.right) {
51 nextLevelQueue.push(node.right);
52 }
53 }
54
55 // Store the maximum value from current level
56 result.push(maxValueInLevel);
57
58 // Clear current queue and move to next level
59 currentLevelQueue.length = 0;
60 currentLevelQueue.push(...nextLevelQueue);
61 }
62
63 return result;
64}
65
Time and Space Complexity
Time Complexity: O(n)
, where n
is the number of nodes in the binary tree.
The algorithm uses BFS (Breadth-First Search) to traverse the tree level by level. Each node in the tree is visited exactly once - it's added to the queue once and processed once when it's dequeued. For each node, we perform constant time operations: comparing values with max()
, checking for left/right children, and adding children to the queue. Since we visit all n
nodes exactly once and perform O(1)
operations for each node, the total time complexity is O(n)
.
Space Complexity: O(n)
, where n
is the number of nodes in the binary tree.
The space complexity is determined by two factors:
- Queue space: In the worst case, the queue stores all nodes at the widest level of the tree. For a complete binary tree, the last level can contain up to
n/2
nodes, making the queue spaceO(n)
. - Output list: The
ans
list stores one value per level. In the worst case (a skewed tree), there could ben
levels, but typically it'sO(log n)
for a balanced tree. However, since the queue dominates the space usage withO(n)
in the worst case, the overall space complexity remainsO(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Preserving Level Boundaries
One of the most common mistakes is failing to properly track which nodes belong to the current level versus the next level. If you don't capture len(queue)
before the inner loop starts, you'll end up processing nodes from multiple levels together.
Incorrect approach:
while queue:
level_max = -inf
# WRONG: This will process ALL nodes, not just current level
while queue:
node = queue.popleft()
level_max = max(level_max, node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level_max)
Correct approach:
while queue:
level_max = -inf
level_size = len(queue) # Capture current level size
for _ in range(level_size): # Process exactly this many nodes
node = queue.popleft()
level_max = max(level_max, node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level_max)
2. Incorrect Initialization of Maximum Value
Using 0 or a small positive number as the initial maximum value can fail when all nodes in a level have negative values.
Incorrect:
level_max = 0 # WRONG: What if all values in level are negative?
Correct:
level_max = -inf # Handles any possible node value # Alternative: Use the first node's value level_max = queue[0].val # Then start loop from index 1
3. Using Regular List Instead of Deque
While a regular Python list works functionally, using list.pop(0)
has O(n) time complexity, making the overall algorithm O(n²) instead of O(n).
Inefficient:
queue = [root] while queue: node = queue.pop(0) # O(n) operation
Efficient:
from collections import deque queue = deque([root]) while queue: node = queue.popleft() # O(1) operation
4. Not Handling Edge Cases Properly
Forgetting to check for an empty tree or assuming the tree has at least one node.
Incorrect:
def largestValues(self, root):
result = []
queue = deque([root]) # Will add None to queue if root is None
while queue:
# Processing None will cause AttributeError
Correct:
def largestValues(self, root):
if not root:
return []
result = []
queue = deque([root])
5. Modifying Queue Length During Iteration
Some developers try to check len(queue)
dynamically within the loop, which changes as nodes are added.
Incorrect:
i = 0
while i < len(queue): # len(queue) changes as we add children!
node = queue.popleft()
# ... add children to queue
i += 1
Correct:
level_size = len(queue) # Capture once before loop
for _ in range(level_size): # Fixed iteration count
node = queue.popleft()
# ... add children to queue
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!