Facebook Pixel

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:

  1. The main function isSubPath checks every node in the tree as a potential starting point
  2. For each node, it calls a helper function dfs that checks if the linked list matches a path starting from that specific node
  3. The dfs function recursively compares the linked list values with the tree path, exploring both left and right children
  4. If the entire linked list is matched (head becomes None), it returns True
  5. 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:

  1. We need to explore paths in a binary tree starting from various nodes
  2. We need to check if a linked list matches any downward path in the tree
  3. DFS allows us to explore each path completely before backtracking
  4. The recursive nature of DFS naturally fits with following the linked list sequence while traversing down the tree
  5. 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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. 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:

  1. Base Case - Success: If head is None, we've successfully matched the entire linked list, return True

  2. Base Case - Failure: If root is None (ran out of tree nodes) or root.val != head.val (values don't match), this path doesn't work, return False

  3. 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

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 Evaluator

Example 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:

  1. 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
    • Continue to left subtree: isSubPath(head=4, root.left=4)
  2. 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
        • Try right: dfs(head=8, root.right=None)
          • root is None, return False
        • Both paths failed, return False
    • The dfs from this node failed, but we still check this node's children
    • Continue to left subtree: isSubPath(head=4, root.left=2)
  3. 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
    • Continue checking children (node with value 1)
    • These won't match either
  4. 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
      • 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
        • 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
  5. Result: The function returns True because we found the path 4 -> 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 us O(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 to m 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 another O(m) in the worst case when checking a path
  • Combined, this gives us O(h + m), but since h can be O(n) in a skewed tree and typically m ≤ n, the space complexity simplifies to O(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 tree
  • dfs/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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More