1372. Longest ZigZag Path in a Binary Tree
Problem Description
You have a binary tree and need to find the longest zigzag path within it.
A zigzag path follows these rules:
- Start at any node in the tree and pick an initial direction (left or right)
- Move to the corresponding child node based on your current direction
- After each move, switch directions (if you went left, next go right; if you went right, next go left)
- Continue alternating directions until you can't move further
The length of a zigzag path is calculated as the number of nodes visited minus 1. This means:
- A single node has a zigzag length of 0
- A path through 2 nodes has a zigzag length of 1
- A path through 3 nodes has a zigzag length of 2, and so on
Your task is to return the length of the longest possible zigzag path that can be found anywhere in the given binary tree.
For example, if you have a path that goes: node â left child â right child â left child, this would visit 4 nodes, giving a zigzag length of 3.
The solution uses a depth-first search (DFS) approach where dfs(root, l, r)
tracks:
l
: the length of the zigzag path coming into the current node from a left mover
: the length of the zigzag path coming into the current node from a right move
At each node, the function:
- Updates the maximum zigzag length seen so far
- Recursively explores the left child (extending the right-zigzag path and resetting the left)
- Recursively explores the right child (extending the left-zigzag path and resetting the right)
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
- We arrive at DFS: Since we're dealing with a tree structure, the flowchart leads us directly to using Depth-First Search.
Conclusion: The flowchart suggests using a DFS (Depth-First Search) approach for this problem.
This makes sense because:
- We need to explore all possible zigzag paths starting from any node in the tree
- At each node, we need to track the zigzag path length coming from different directions (left or right)
- DFS allows us to recursively traverse the tree while maintaining state about the current zigzag path
- We can efficiently compute the maximum zigzag length by exploring each subtree and tracking the best path found so far
The DFS pattern is ideal here as it naturally follows the tree structure and allows us to build up zigzag paths incrementally as we traverse from parent to child nodes.
Intuition
The key insight is that at any node in the tree, we can arrive from two possible directions - either from a left move or a right move from the parent. This arrival direction determines what our next move must be to continue the zigzag pattern.
Think of it this way: if we just moved left to reach the current node, our next move (if we continue) must be to the right. Similarly, if we arrived by moving right, we must go left next. This alternating pattern is the essence of zigzag.
At each node, we need to track two pieces of information:
- The length of the zigzag path if we arrived here by going left (from parent's perspective)
- The length of the zigzag path if we arrived here by going right (from parent's perspective)
Why two values? Because depending on how we arrived at a node, we have different continuation options:
- If we came from a left move (parent went left to reach us), we can only continue the zigzag by going right
- If we came from a right move (parent went right to reach us), we can only continue the zigzag by going left
When we visit a node's left child, we're making a left move. This means:
- The zigzag path that was coming from the right (
r
) can continue, so we passr + 1
- Any zigzag path coming from the left cannot continue (it would require two consecutive left moves), so we reset it to
0
Similarly, when visiting the right child:
- The zigzag path coming from the left (
l
) can continue, so we passl + 1
- The path from the right resets to
0
Starting from the root with dfs(root, 0, 0)
makes sense because the root has no parent, so there's no existing zigzag path coming into it from either direction.
As we traverse the tree, we continuously update our answer with the maximum of the current answer and the two path lengths at each node, ensuring we capture the longest zigzag path anywhere in the tree.
Learn more about Tree, Depth-First Search, Dynamic Programming and Binary Tree patterns.
Solution Approach
The solution implements a DFS traversal with state tracking at each node. Here's how the implementation works:
Core DFS Function:
The function dfs(root, l, r)
takes three parameters:
root
: the current node being visitedl
: length of the zigzag path arriving from a left mover
: length of the zigzag path arriving from a right move
Base Case:
If the current node is None
, we simply return without doing anything - this handles leaf nodes' null children.
Update Maximum:
At each node, we update the global answer with ans = max(ans, l, r)
. This captures the longest zigzag path that passes through the current node, comparing:
- The current maximum answer
- The zigzag length if we arrived from the left (
l
) - The zigzag length if we arrived from the right (
r
)
Recursive Calls: The key insight is in how we make recursive calls to children:
-
Left child traversal:
dfs(root.left, r + 1, 0)
- We're moving left from the current node
- If we had a zigzag path coming from the right (
r
), it can continue, so we passr + 1
- Any path coming from the left cannot continue (would break zigzag), so we pass
0
-
Right child traversal:
dfs(root.right, 0, l + 1)
- We're moving right from the current node
- If we had a zigzag path coming from the left (
l
), it can continue, so we passl + 1
- Any path coming from the right cannot continue, so we pass
0
Initialization:
- We use a
nonlocal ans
variable to track the maximum zigzag length found - Start the traversal with
dfs(root, 0, 0)
since the root has no parent (no existing zigzag path) - Return the final answer
Why This Works: The algorithm ensures that at every node, we consider all possible zigzag paths passing through it. By maintaining separate counters for paths arriving from different directions and properly updating them based on the zigzag rules, we guarantee finding the maximum length zigzag path in the entire tree.
The time complexity is O(n)
where n
is the number of nodes, as we visit each node exactly once. The space complexity is O(h)
where h
is the height of the tree, due to the recursion stack.
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 small example to illustrate the solution approach.
Consider this binary tree:
1 / \ 2 3 / \ 4 5 / 6
We'll trace through the DFS calls to see how the algorithm finds the longest zigzag path.
Initial Call: dfs(node 1, 0, 0)
- At root node 1,
ans = max(0, 0, 0) = 0
- Call
dfs(node 2, 0+1, 0)
for left child - Call
dfs(node 3, 0, 0+1)
for right child
Processing Node 2: dfs(node 2, 1, 0)
- We arrived here by going left (l=1, r=0)
- Update
ans = max(0, 1, 0) = 1
- Call
dfs(node 4, 0+1, 0)
for left child - Node 2 has no right child
Processing Node 4: dfs(node 4, 1, 0)
- We arrived by going left again (l=1, r=0)
- Update
ans = max(1, 1, 0) = 1
- Call
dfs(node 6, 0+1, 0)
for left child - Node 4 has no right child
Processing Node 6: dfs(node 6, 1, 0)
- Leaf node, arrived by going left (l=1, r=0)
- Update
ans = max(1, 1, 0) = 1
- Both children are null, return
Back to Node 3: dfs(node 3, 0, 1)
- We arrived by going right from node 1 (l=0, r=1)
- Update
ans = max(1, 0, 1) = 1
- Node 3 has no left child
- Call
dfs(node 5, 0, 0+1)
for right child
Processing Node 5: dfs(node 5, 0, 1)
- We arrived by going right (l=0, r=1)
- Update
ans = max(1, 0, 1) = 1
- Both children are null, return
The maximum zigzag length found is 1, which corresponds to any two-node path (like 1â2, 1â3, 2â4, etc.).
Now let's modify the tree to see a longer zigzag:
1
/
2
\
3
/
4
Trace for the zigzag path:
dfs(node 1, 0, 0)
: Start at rootdfs(node 2, 1, 0)
: Went left to node 2, so r=1 for continuing zigzagdfs(node 3, 0, 2)
: Went right to node 3, previous r becomes l+1 = 0+1 = 2dfs(node 4, 3, 0)
: Went left to node 4, previous l becomes r+1 = 2+1 = 3- At node 4:
ans = max(previous, 3, 0) = 3
The zigzag path 1â2â3â4 visits 4 nodes, giving a length of 3 (nodes - 1). The algorithm correctly identifies this by tracking how the zigzag length grows as we alternate directions: left (0â1), right (1â2), left (2â3).
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
8class Solution:
9 def longestZigZag(self, root: TreeNode) -> int:
10 """
11 Find the longest zigzag path in a binary tree.
12 A zigzag path alternates between left and right child nodes.
13
14 Args:
15 root: The root of the binary tree
16
17 Returns:
18 The length of the longest zigzag path
19 """
20
21 def dfs(node: TreeNode, left_length: int, right_length: int) -> None:
22 """
23 Traverse the tree and track zigzag path lengths.
24
25 Args:
26 node: Current node being processed
27 left_length: Length of zigzag path if we came from a right child
28 right_length: Length of zigzag path if we came from a left child
29 """
30 # Base case: if node is None, return
31 if node is None:
32 return
33
34 # Update the maximum zigzag length found so far
35 nonlocal max_length
36 max_length = max(max_length, left_length, right_length)
37
38 # Move to left child:
39 # - If we came from left (right_length), we continue the zigzag
40 # - Reset the other direction to 0
41 dfs(node.left, right_length + 1, 0)
42
43 # Move to right child:
44 # - If we came from right (left_length), we continue the zigzag
45 # - Reset the other direction to 0
46 dfs(node.right, 0, left_length + 1)
47
48 # Initialize the maximum zigzag length
49 max_length = 0
50
51 # Start DFS from root with both directions at 0
52 dfs(root, 0, 0)
53
54 return max_length
55
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 // Global variable to track the maximum zigzag path length
18 private int maxZigZagLength;
19
20 /**
21 * Finds the longest zigzag path in a binary tree.
22 * A zigzag path alternates between left and right child nodes.
23 *
24 * @param root The root node of the binary tree
25 * @return The length of the longest zigzag path
26 */
27 public int longestZigZag(TreeNode root) {
28 maxZigZagLength = 0;
29 // Start DFS traversal with initial left and right counts as 0
30 dfs(root, 0, 0);
31 return maxZigZagLength;
32 }
33
34 /**
35 * Performs depth-first search to calculate zigzag paths.
36 *
37 * @param node The current node being processed
38 * @param leftCount The length of zigzag path if we came from right (going left next)
39 * @param rightCount The length of zigzag path if we came from left (going right next)
40 */
41 private void dfs(TreeNode node, int leftCount, int rightCount) {
42 // Base case: if node is null, return
43 if (node == null) {
44 return;
45 }
46
47 // Update the maximum zigzag length found so far
48 maxZigZagLength = Math.max(maxZigZagLength, Math.max(leftCount, rightCount));
49
50 // Traverse left child:
51 // - If we go left, the previous zigzag came from right, so pass rightCount + 1
52 // - Reset the other direction to 0 since we're starting a new path
53 dfs(node.left, rightCount + 1, 0);
54
55 // Traverse right child:
56 // - If we go right, the previous zigzag came from left, so pass leftCount + 1
57 // - Reset the other direction to 0 since we're starting a new path
58 dfs(node.right, 0, leftCount + 1);
59 }
60}
61
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 int longestZigZag(TreeNode* root) {
15 maxZigZagLength = 0;
16 dfs(root, 0, 0);
17 return maxZigZagLength;
18 }
19
20private:
21 int maxZigZagLength; // Stores the maximum zigzag path length found
22
23 /**
24 * Performs depth-first search to find the longest zigzag path
25 * @param node: Current node being processed
26 * @param leftLength: Length of zigzag path if we came from right (next move should be left)
27 * @param rightLength: Length of zigzag path if we came from left (next move should be right)
28 */
29 void dfs(TreeNode* node, int leftLength, int rightLength) {
30 // Base case: if node is null, return
31 if (node == nullptr) {
32 return;
33 }
34
35 // Update the maximum zigzag length with current path lengths
36 maxZigZagLength = std::max(maxZigZagLength, std::max(leftLength, rightLength));
37
38 // Move to left child:
39 // - If we go left, the previous right length + 1 becomes the new left length
40 // - Reset right length to 0 since we're starting a new potential zigzag
41 dfs(node->left, rightLength + 1, 0);
42
43 // Move to right child:
44 // - If we go right, the previous left length + 1 becomes the new right length
45 // - Reset left length to 0 since we're starting a new potential zigzag
46 dfs(node->right, 0, leftLength + 1);
47 }
48};
49
1/**
2 * Definition for a binary tree node.
3 */
4class TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8 constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.left = (left === undefined ? null : left);
11 this.right = (right === undefined ? null : right);
12 }
13}
14
15// Stores the maximum zigzag path length found
16let maxZigZagLength: number;
17
18/**
19 * Finds the longest zigzag path in a binary tree
20 * @param root - The root node of the binary tree
21 * @returns The length of the longest zigzag path
22 */
23function longestZigZag(root: TreeNode | null): number {
24 maxZigZagLength = 0;
25 dfs(root, 0, 0);
26 return maxZigZagLength;
27}
28
29/**
30 * Performs depth-first search to find the longest zigzag path
31 * @param node - Current node being processed
32 * @param leftLength - Length of zigzag path if we came from right (next move should be left)
33 * @param rightLength - Length of zigzag path if we came from left (next move should be right)
34 */
35function dfs(node: TreeNode | null, leftLength: number, rightLength: number): void {
36 // Base case: if node is null, return
37 if (node === null) {
38 return;
39 }
40
41 // Update the maximum zigzag length with current path lengths
42 maxZigZagLength = Math.max(maxZigZagLength, Math.max(leftLength, rightLength));
43
44 // Move to left child:
45 // - If we go left, the previous right length + 1 becomes the new left length
46 // - Reset right length to 0 since we're starting a new potential zigzag
47 dfs(node.left, rightLength + 1, 0);
48
49 // Move to right child:
50 // - If we go right, the previous left length + 1 becomes the new right length
51 // - Reset left length to 0 since we're starting a new potential zigzag
52 dfs(node.right, 0, leftLength + 1);
53}
54
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the binary tree. The algorithm performs a depth-first search (DFS) traversal of the tree, visiting each node exactly once. At each node, it performs constant time operations (updating the maximum value and making recursive calls), so the overall time complexity is linear with respect to the number of nodes.
Space Complexity: O(h)
where h
is the height of the binary tree. The space complexity is determined by the recursive call stack. In the worst case of a skewed tree (essentially a linked list), the height h
equals n
, making the space complexity O(n)
. In the best case of a perfectly balanced tree, the height is log(n)
, making the space complexity O(log n)
. The algorithm uses a constant amount of extra space for variables (ans
, l
, r
) apart from the recursion stack.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Swapping the Parameters in Recursive Calls
One of the most common mistakes is mixing up which parameter to increment and which to reset when making recursive calls. Many developers incorrectly write:
Incorrect Implementation:
# WRONG: Parameters are swapped dfs(node.left, left_length + 1, 0) # Should be right_length + 1 dfs(node.right, 0, right_length + 1) # Should be left_length + 1
Why This Fails: The zigzag pattern requires alternating directions. When you move left from a node, you can only continue a zigzag path that arrived from the right (and vice versa). Using the wrong parameter breaks this alternation logic.
Correct Implementation:
# When moving left, continue the path that came from right dfs(node.left, right_length + 1, 0) # When moving right, continue the path that came from left dfs(node.right, 0, left_length + 1)
2. Forgetting to Initialize Both Directions to 0 at the Root
Another pitfall is starting with non-zero values for the initial DFS call:
Incorrect:
dfs(root, 1, 1) # WRONG: Assumes the root already has path length 1
Why This Fails: The root node has no parent, so there's no existing zigzag path coming into it. Starting with non-zero values would incorrectly count the root as part of a non-existent parent path.
Correct:
dfs(root, 0, 0) # Start with no existing path from either direction
3. Not Using Nonlocal for the Answer Variable
Forgetting to declare the answer variable as nonlocal
leads to scope issues:
Incorrect:
def longestZigZag(self, root: TreeNode) -> int:
def dfs(node, left_length, right_length):
if node is None:
return
max_length = max(max_length, left_length, right_length) # ERROR: UnboundLocalError
# ...
max_length = 0
dfs(root, 0, 0)
return max_length
Correct:
def dfs(node, left_length, right_length):
if node is None:
return
nonlocal max_length # Properly declare access to outer scope variable
max_length = max(max_length, left_length, right_length)
4. Misunderstanding the Path Length Calculation
Some developers mistakenly think the path length equals the number of nodes visited:
Incorrect Understanding:
- Path through 3 nodes = length 3 â
Correct Understanding:
- Path through 3 nodes = length 2 (number of edges) â
- The algorithm handles this by incrementing the count when moving to children
The solution correctly handles this by passing length + 1
to children, effectively counting edges rather than nodes.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Donât Miss This!