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)wherexstarts 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 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 591/** 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} 701/** 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}; 551/** 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} 65Solution 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
qusingdeque([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 = -infto 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.
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/2nodes, making the queue spaceO(n). - Output list: The
anslist stores one value per level. In the worst case (a skewed tree), there could benlevels, 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
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
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!