1367. Linked List in Binary Tree
Problem Description
You are given a binary tree and a linked list. Your task is to determine if the linked list represents a valid downward path somewhere in the binary tree.
A downward path in the binary tree means:
- It starts at any node in the tree (not necessarily the root)
- It moves only downward (from parent to child nodes)
- It can go either left or right at each step
The linked list matches a path if:
- Starting from the linked list's head, each value in the linked list corresponds to consecutive nodes along a downward path in the tree
- All values in the linked list must be matched in order
For example, if the linked list is 4 -> 2 -> 8
and the binary tree contains a path like 1 -> 4 -> 2 -> 8
, then the linked list matches the subpath 4 -> 2 -> 8
.
The function should return True
if such a matching downward path exists anywhere in the binary tree, and False
otherwise.
The solution uses a two-level approach:
- The main function
isSubPath
checks every node in the tree as a potential starting point - For each node, it calls a helper function
dfs
that checks if the linked list matches a path starting from that specific node - The
dfs
function recursively compares the linked list values with the tree path, exploring both left and right children - If the entire linked list is matched (head becomes
None
), it returnsTrue
- If the tree path ends before the linked list is fully matched, or if values don't match, it returns
False
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 involves a binary tree structure where we need to traverse from parent nodes to child nodes.
DFS
- Yes: We arrive at DFS (Depth-First Search) as our algorithm choice.
Conclusion: The flowchart suggests using a DFS approach for this problem.
This makes perfect sense because:
- We need to explore paths in a binary tree starting from various nodes
- We need to check if a linked list matches any downward path in the tree
- DFS allows us to explore each path completely before backtracking
- The recursive nature of DFS naturally fits with following the linked list sequence while traversing down the tree
- We can use DFS both to traverse the tree to find potential starting points and to verify if the linked list matches a path from each starting point
The solution implements DFS in two ways:
- The outer DFS traverses the entire tree to try each node as a potential starting point
- The inner DFS (the
dfs
helper function) checks if the linked list matches a path starting from a specific node
Intuition
The key insight is that we need to find if the linked list appears as a consecutive sequence somewhere in the tree, following a downward path. This immediately suggests we need to check multiple starting points - the linked list path could start at any node in the tree, not just the root.
Think of it like finding a substring in a string, but in a tree structure. Just as you would check every position in a string as a potential starting point for your substring, we need to check every node in the tree as a potential starting point for our linked list path.
This naturally leads to a two-level search strategy:
-
First level: Traverse the entire tree to try each node as a potential starting point. This ensures we don't miss any possible matching path that might start deep in the tree.
-
Second level: For each potential starting node, check if we can match the entire linked list by following a path downward from that node.
The second level check is straightforward - we compare values one by one:
- If the current tree node matches the current linked list node, we continue checking with the next linked list node and either the left or right child
- If we successfully match all linked list nodes (reach the end of the list), we've found our answer
- If values don't match or we run out of tree nodes before finishing the linked list, this path doesn't work
Why DFS? Because we need to explore complete paths. When checking if a linked list matches from a particular starting node, we need to go all the way down one path before trying another. BFS would jump between different paths at the same level, which doesn't help us verify consecutive sequences.
The recursive nature of DFS also elegantly handles the backtracking - if one path doesn't work (say going left), we automatically try the other path (going right) through the or
operation in our recursive calls.
Learn more about Tree, Depth-First Search, Linked List and Binary Tree patterns.
Solution Approach
The solution implements a recursive approach with two functions working together:
Main Function: isSubPath
This function traverses the entire binary tree to try each node as a potential starting point for matching the linked list. For each node in the tree, it:
- First checks if the current node is
None
(base case - no more nodes to check) - Calls the helper function
dfs
to check if the linked list matches starting from this node - Recursively checks the left and right subtrees as potential starting points
- Returns
True
if any starting point results in a match
The function uses the logical or
operator to combine three checks:
dfs(head, root) or self.isSubPath(head, root.left) or self.isSubPath(head, root.right)
Helper Function: dfs
This function verifies if the linked list matches a downward path starting from a specific tree node. The logic follows these steps:
-
Base Case - Success: If
head is None
, we've successfully matched the entire linked list, returnTrue
-
Base Case - Failure: If
root is None
(ran out of tree nodes) orroot.val != head.val
(values don't match), this path doesn't work, returnFalse
-
Recursive Case: If the current values match, continue checking:
- Try matching the rest of the linked list (
head.next
) with the left child (root.left
) - Try matching the rest of the linked list (
head.next
) with the right child (root.right
) - Return
True
if either path works
- Try matching the rest of the linked list (
The recursion naturally handles backtracking - if the left path doesn't work, it tries the right path automatically.
Time Complexity: O(N × M) where N is the number of nodes in the tree and M is the length of the linked list. In the worst case, we check M nodes for each of the N tree nodes.
Space Complexity: O(H) where H is the height of the tree, due to the recursive call stack. In the worst case (skewed tree), this could be 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 a concrete example to illustrate how the solution works.
Given:
- Linked List:
4 -> 2 -> 8
- Binary Tree:
1 / \ 4 4 / \ 2 2 / \ 1 8
Step-by-step execution:
-
Start with root (1):
- Call
isSubPath(head=4, root=1)
- First check:
dfs(head=4, root=1)
- Values don't match (4 ≠ 1), return
False
- Values don't match (4 ≠ 1), return
- Continue to left subtree:
isSubPath(head=4, root.left=4)
- Call
-
Check left child of root (node with value 4):
- Call
isSubPath(head=4, root=4)
- First check:
dfs(head=4, root=4)
- Values match (4 = 4)! Continue with
head.next=2
- Try left:
dfs(head=2, root.left=2)
- Values match (2 = 2)! Continue with
head.next=8
- Try left:
dfs(head=8, root.left=1)
- Values don't match (8 ≠ 1), return
False
- Values don't match (8 ≠ 1), return
- Try right:
dfs(head=8, root.right=None)
- root is None, return
False
- root is None, return
- Both paths failed, return
False
- Values match (2 = 2)! Continue with
- Values match (4 = 4)! Continue with
- The dfs from this node failed, but we still check this node's children
- Continue to left subtree:
isSubPath(head=4, root.left=2)
- Call
-
Check node with value 2 (left subtree):
- Call
isSubPath(head=4, root=2)
- First check:
dfs(head=4, root=2)
- Values don't match (4 ≠ 2), return
False
- Values don't match (4 ≠ 2), return
- Continue checking children (node with value 1)
- These won't match either
- Call
-
Eventually check right child of root (the other node with value 4):
- Call
isSubPath(head=4, root=4)
- First check:
dfs(head=4, root=4)
- Values match (4 = 4)! Continue with
head.next=2
- Try left:
dfs(head=2, root.left=None)
- root is None, return
False
- root is None, return
- Try right:
dfs(head=2, root.right=2)
- Values match (2 = 2)! Continue with
head.next=8
- Try left:
dfs(head=8, root.left=None)
- root is None, return
False
- root is None, return
- Try right:
dfs(head=8, root.right=8)
- Values match (8 = 8)! Continue with
head.next=None
- head is None - we've matched the entire list!
- Return
True
- Values match (8 = 8)! Continue with
- Values match (2 = 2)! Continue with
- Values match (4 = 4)! Continue with
- Call
-
Result: The function returns
True
because we found the path4 -> 2 -> 8
in the right subtree.
Key observations from this walkthrough:
- We check every node as a potential starting point
- When we find a matching value, we explore both left and right paths
- We only return
True
when we match the entire linked list - The recursion handles backtracking automatically when a path fails
Solution Implementation
1# Definition for singly-linked list.
2# class ListNode:
3# def __init__(self, val=0, next=None):
4# self.val = val
5# self.next = next
6
7# Definition for a binary tree node.
8# class TreeNode:
9# def __init__(self, val=0, left=None, right=None):
10# self.val = val
11# self.left = left
12# self.right = right
13
14from typing import Optional
15
16class Solution:
17 def isSubPath(self, head: Optional[ListNode], root: Optional[TreeNode]) -> bool:
18 """
19 Check if a linked list is a subpath in a binary tree.
20 A subpath is a consecutive sequence starting from any node downwards.
21
22 Args:
23 head: Head of the linked list
24 root: Root of the binary tree
25
26 Returns:
27 True if the linked list is a subpath in the tree, False otherwise
28 """
29
30 def check_path_from_node(list_node: Optional[ListNode], tree_node: Optional[TreeNode]) -> bool:
31 """
32 Helper function to check if the linked list matches a path starting from the current tree node.
33
34 Args:
35 list_node: Current node in the linked list
36 tree_node: Current node in the binary tree
37
38 Returns:
39 True if the remaining linked list matches a path from this tree node
40 """
41 # Base case: reached end of linked list, path found
42 if list_node is None:
43 return True
44
45 # Base case: tree node is None or values don't match
46 if tree_node is None or tree_node.val != list_node.val:
47 return False
48
49 # Recursively check if the rest of the list matches either left or right subtree
50 return (check_path_from_node(list_node.next, tree_node.left) or
51 check_path_from_node(list_node.next, tree_node.right))
52
53 # Base case: empty tree
54 if root is None:
55 return False
56
57 # Check if path starts from current node, or exists in left/right subtree
58 return (check_path_from_node(head, root) or
59 self.isSubPath(head, root.left) or
60 self.isSubPath(head, root.right))
61
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 * int val;
5 * ListNode next;
6 * ListNode() {}
7 * ListNode(int val) { this.val = val; }
8 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11/**
12 * Definition for a binary tree node.
13 * public class TreeNode {
14 * int val;
15 * TreeNode left;
16 * TreeNode right;
17 * TreeNode() {}
18 * TreeNode(int val) { this.val = val; }
19 * TreeNode(int val, TreeNode left, TreeNode right) {
20 * this.val = val;
21 * this.left = left;
22 * this.right = right;
23 * }
24 * }
25 */
26class Solution {
27 /**
28 * Checks if there exists a downward path in the binary tree that matches the linked list.
29 * The path can start from any node in the tree.
30 *
31 * @param head The head of the linked list to match
32 * @param root The root of the binary tree to search in
33 * @return true if the linked list is a subpath in the tree, false otherwise
34 */
35 public boolean isSubPath(ListNode head, TreeNode root) {
36 // Base case: if tree is empty, no path can exist
37 if (root == null) {
38 return false;
39 }
40
41 // Check three possibilities:
42 // 1. The path starts from current node
43 // 2. The path exists in the left subtree
44 // 3. The path exists in the right subtree
45 return dfs(head, root) || isSubPath(head, root.left) || isSubPath(head, root.right);
46 }
47
48 /**
49 * Helper method to check if the linked list matches a downward path starting from the current tree node.
50 * This method must match the entire remaining linked list from the current position.
51 *
52 * @param head The current node in the linked list to match
53 * @param root The current node in the tree being checked
54 * @return true if the remaining linked list matches a downward path from this tree node
55 */
56 private boolean dfs(ListNode head, TreeNode root) {
57 // Base case: reached end of linked list, all nodes matched successfully
58 if (head == null) {
59 return true;
60 }
61
62 // Base case: tree node is null or values don't match
63 if (root == null || head.val != root.val) {
64 return false;
65 }
66
67 // Recursively check if the rest of the linked list matches
68 // either the left child path or the right child path
69 return dfs(head.next, root.left) || dfs(head.next, root.right);
70 }
71}
72
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 * int val;
5 * ListNode *next;
6 * ListNode() : val(0), next(nullptr) {}
7 * ListNode(int x) : val(x), next(nullptr) {}
8 * ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11/**
12 * Definition for a binary tree node.
13 * struct TreeNode {
14 * int val;
15 * TreeNode *left;
16 * TreeNode *right;
17 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
18 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
19 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
20 * };
21 */
22class Solution {
23public:
24 /**
25 * Check if there exists a downward path in the binary tree that matches the linked list
26 * @param head - The head of the linked list to match
27 * @param root - The root of the binary tree to search in
28 * @return true if the linked list is a subpath in the tree, false otherwise
29 */
30 bool isSubPath(ListNode* head, TreeNode* root) {
31 // Base case: empty tree cannot contain any path
32 if (root == nullptr) {
33 return false;
34 }
35
36 // Check three possibilities:
37 // 1. The path starts from current node
38 // 2. The path exists in the left subtree
39 // 3. The path exists in the right subtree
40 return matchPath(head, root) ||
41 isSubPath(head, root->left) ||
42 isSubPath(head, root->right);
43 }
44
45private:
46 /**
47 * Helper function to check if the linked list matches a path starting from the current tree node
48 * @param listNode - Current node in the linked list
49 * @param treeNode - Current node in the binary tree
50 * @return true if remaining list matches a downward path from current tree node
51 */
52 bool matchPath(ListNode* listNode, TreeNode* treeNode) {
53 // Successfully matched entire linked list
54 if (listNode == nullptr) {
55 return true;
56 }
57
58 // Tree ended before list or values don't match
59 if (treeNode == nullptr || listNode->val != treeNode->val) {
60 return false;
61 }
62
63 // Continue matching with next list node and either left or right child
64 return matchPath(listNode->next, treeNode->left) ||
65 matchPath(listNode->next, treeNode->right);
66 }
67};
68
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 * val: number
5 * next: ListNode | null
6 * constructor(val?: number, next?: ListNode | null) {
7 * this.val = (val===undefined ? 0 : val)
8 * this.next = (next===undefined ? null : next)
9 * }
10 * }
11 */
12
13/**
14 * Definition for a binary tree node.
15 * class TreeNode {
16 * val: number
17 * left: TreeNode | null
18 * right: TreeNode | null
19 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
20 * this.val = (val===undefined ? 0 : val)
21 * this.left = (left===undefined ? null : left)
22 * this.right = (right===undefined ? null : right)
23 * }
24 * }
25 */
26
27/**
28 * Performs depth-first search to check if the linked list matches a downward path in the tree
29 * @param head - Current node in the linked list
30 * @param root - Current node in the binary tree
31 * @returns True if the remaining linked list matches a downward path from the current tree node
32 */
33const dfs = (head: ListNode | null, root: TreeNode | null): boolean => {
34 // Base case: If we've reached the end of the linked list, we found a valid path
35 if (head === null) {
36 return true;
37 }
38
39 // If tree node is null or values don't match, this path is invalid
40 if (root === null || head.val !== root.val) {
41 return false;
42 }
43
44 // Recursively check if the rest of the linked list matches either left or right subtree
45 return dfs(head.next, root.left) || dfs(head.next, root.right);
46};
47
48/**
49 * Checks if a linked list is a subpath in a binary tree
50 * @param head - Head of the linked list
51 * @param root - Root of the binary tree
52 * @returns True if the linked list represents a downward path in the tree
53 */
54function isSubPath(head: ListNode | null, root: TreeNode | null): boolean {
55 // Base case: If tree is empty, no subpath exists
56 if (root === null) {
57 return false;
58 }
59
60 // Check if path starts from current node, or recursively check left and right subtrees
61 return dfs(head, root) || isSubPath(head, root.left) || isSubPath(head, root.right);
62}
63
Time and Space Complexity
Time Complexity: O(n × m)
where n
is the number of nodes in the binary tree and m
is the length of the linked list.
The analysis breaks down as follows:
- The main function
isSubPath
visits each node in the binary tree at most once, giving usO(n)
iterations - At each tree node, we potentially call the helper function
dfs
which traverses the linked list to check for a matching path - The
dfs
function can traverse up tom
nodes in the linked list in the worst case - Therefore, the overall time complexity is
O(n × m)
Note: The reference answer simplifies this to O(n²)
which assumes the worst case where m ≈ n
(the linked list length is comparable to the tree size).
Space Complexity: O(h)
where h
is the height of the binary tree, which is O(n)
in the worst case.
The space complexity comes from:
- The recursion stack for
isSubPath
which can go as deep as the height of the tree:O(h)
- The recursion stack for
dfs
which can add anotherO(m)
in the worst case when checking a path - Combined, this gives us
O(h + m)
, but sinceh
can beO(n)
in a skewed tree and typicallym ≤ n
, the space complexity simplifies toO(n)
in the worst case
The reference answer's O(n)
space complexity aligns with this analysis for the worst-case scenario of an unbalanced tree.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Path Continuity Requirements
A critical mistake is misunderstanding that once we start matching the linked list with a tree path, we must continue matching consecutive nodes without "restarting" the linked list in the middle of the path.
Incorrect Approach:
def dfs(head, root):
if head is None:
return True
if root is None:
return False
# WRONG: This allows restarting the linked list matching at any point
if root.val == head.val:
return dfs(head.next, root.left) or dfs(head.next, root.right)
else:
# This incorrectly tries to restart matching from the beginning
return dfs(head, root.left) or dfs(head, root.right)
This faulty logic would incorrectly return True
for cases where the linked list values appear in the tree but not as a consecutive downward path.
Correct Approach: The solution correctly separates the two concerns:
isSubPath
: Tries different starting points in the treedfs
/check_path_from_node
: Once started, must match the entire linked list consecutively
2. Not Handling Edge Cases Properly
Common Edge Case Mistakes:
- Not checking if the tree is empty before processing
- Not handling when the linked list has only one node
- Forgetting that the path can start from ANY node, not just the root
Example of Missing Edge Case Handling:
def isSubPath(self, head, root):
# WRONG: Doesn't handle empty tree case first
return (check_path_from_node(head, root) or
self.isSubPath(head, root.left) or # This would crash if root is None
self.isSubPath(head, root.right))
Solution:
Always check for None
cases first:
if root is None: return False
3. Inefficient Repeated Traversals
While the current solution is correct, a common pitfall when trying to optimize is to accidentally break the logic. Some might try to combine the traversal and matching in one function, leading to incorrect results.
Tempting but Wrong Optimization:
def isSubPath(self, head, root, original_head=None):
if original_head is None:
original_head = head
# Trying to do everything in one function often leads to bugs
# Missing proper reset logic or incorrect state management
Better Optimization (if needed): Instead of trying to merge functions, consider using memoization or converting the linked list to an array first for easier pattern matching algorithms like KMP, though this adds space complexity.
4. Incorrect Short-Circuit Logic
Wrong Use of Logical Operators:
# WRONG: Using 'and' instead of 'or' return check_path_from_node(head, root) and ( self.isSubPath(head, root.left) or self.isSubPath(head, root.right))
This would require the path to exist from the current node AND also exist in a subtree, which is logically incorrect.
Correct Logic:
Use or
to check if the path exists in ANY of these locations:
- Starting from current node
- Anywhere in the left subtree
- Anywhere in the right subtree
In a binary min heap, the minimum element can be found in:
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
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Want a Structured Path to Master System Design Too? Don’t Miss This!