5 Best Ways to Determine If a Linked List Is Present in a Given Binary Tree in Python

💡 Problem Formulation: You’re tasked to write Python code to determine whether a linked list is represented within the paths of a given binary tree. This problem is common in the intersection of data structures. For example, suppose you have a binary tree and are given a linked list [1 -> 5 -> 6]. The goal is to check if there’s a path in the binary tree with the values 1 -> 5 -> 6, following the proper linked list order.

Method 1: Recursive Tree Traversal

This method involves using a recursive function to traverse through the binary tree. The recursive function compares each node with the current node in the linked list. If they match, the linked list advances, and the function continues the traversal until the linked list is successfully traversed or the tree path ends.

Here’s an example:

class TreeNode:
    def __init__(self, value=0, left=None, right=None):
        self.val = value
        self.left = left
        self.right = right

class ListNode:
    def __init__(self, value=0, next=None):
        self.val = value
        self.next = next

def isSubPath(head, root):
    if not head:
        return True
    if not root:
        return False
    if root.val == head.val:
        return isSubPath(head.next, root.left) or isSubPath(head.next, root.right)
    return isSubPath(head, root.left) or isSubPath(head, root.right)

# Example Usage
bin_tree = TreeNode(1, TreeNode(5, TreeNode(6)), TreeNode(1, TreeNode(4)))
linked_list = ListNode(5, ListNode(6))

print(isSubPath(linked_list, bin_tree))

Output:

True

This snippet defines two classes, TreeNode and ListNode, for the binary tree and linked list nodes, respectively. The function isSubPath is a recursive function that checks if the linked list is a subpath in the binary tree. It steps through both data structures, comparing corresponding nodes, and using logical or to allow traversal down both left and right paths. The example provided would return True, indicating that the linked list is indeed a subpath of the binary tree.

Method 2: String Serialization and Substring Check

This method uses the concept of serialization. It converts both the linked list and the binary tree paths into string representations. Afterwards, it simply checks if the string representing the linked list is a substring of the binary tree string representation.

Here’s an example:

def serializeTree(root):
    return "!" + str(root.val) + "!" + serializeTree(root.left) + serializeTree(root.right) if root else "#"

def serializeLinkedList(head):
    return "!" + "!".join([str(head.val)] + serializeLinkedList(head.next)) if head else ""

def isSubPath(head, root):
    return serializeLinkedList(head) in serializeTree(root)

# Example Usage
print(isSubPath(linked_list, bin_tree))

Output:

True

The functions serializeTree and serializeLinkedList generate unique string representations of the binary tree and linked list by separating node values with a delimiter "!". This ensures that only exact matches count, avoiding false positives due to substring collisions. The isSubPath function checks if the linked list’s string appears within the binary tree’s string.

Method 3: Depth-First Search (DFS) with Path Recording

This method does a depth-first search on the binary tree, recording the path as it goes. It periodically checks against the linked list to see if the current path from root to leaf matches the list of values in the given linked list.

Here’s an example:

def isSubPath(head, root, path=None):
    if not head:
        return True
    if not root:
        return False
    path = path + [root.val] if path else [root.val]
    if path[-len(head):] == [node.val for node in iterLinkedList(head)]:
        return True
    return isSubPath(head, root.left, path) or isSubPath(head, root.right, path)

def iterLinkedList(head):
    while head:
        yield head
        head = head.next

print(isSubPath(linked_list, bin_tree))

Output:

True

In isSubPath, path is a list that records the values of the visited nodes. The helper generator function iterLinkedList, converts the linked list into a list of values that can be easily compared with slices of the path list. This method directly compares values rather than tree and list nodes.

Method 4: Utilizing Parent References

If the binary tree nodes have references to their parents, this allows for a different approach where you can check any given leaf-to-root path against the linked list by moving upward from leaf to root.

Here’s an example:

class TreeNodeModified(TreeNode):
    def __init__(self, value=0, left=None, right=None, parent=None):
        super().__init__(value, left, right)
        self.parent = parent

def isSubPathFromLeaf(head, leaf):
    while leaf and head and leaf.val == head.val:
        leaf = leaf.parent
        head = head.next
    return not head

def findLeavesAndCheck(root, head):
    if not root:
        return False
    if isSubPathFromLeaf(head, root):
        return True
    return findLeavesAndCheck(root.left, head) or findLeavesAndCheck(root.right, head)

print(findLeavesAndCheck(bin_tree, linked_list))

Output:

True

The modified TreeNodeModified class includes parent references. The function isSubPathFromLeaf checks if a linked list is a path from a given leaf node upwards. findLeavesAndCheck assists by traversing to every leaf before invoking isSubPathFromLeaf.

Bonus One-Liner Method 5: Leveraging itertools

The Python itertools library can be used strategically to generate all possible paths in the binary tree. These paths can then be matched against the linked list.

Here’s an example:

import itertools

def binaryTreePaths(root):
    stack = [(root, "")]
    paths = []
    while stack:
        node, path = stack.pop()
        if not node.left and not node.right:
            paths.append(path + str(node.val))
        if node.right:
            stack.append((node.right, path + str(node.val) + "->"))
        if node.left:
            stack.append((node.left, path + str(node.val) + "->"))
    return paths

linked_list_vals = [str(n.val) for n in iterLinkedList(linked_list)]
isSubPath = any('->'.join(linked_list_vals) in path for path in binaryTreePaths(bin_tree))

print(isSubPath)

Output:

True

The function binaryTreePaths uses a stack to perform a preorder traversal of the tree, constructing all possible paths as strings. The one-liner isSubPath checks if the concatenated string of linked list values appears in any path strings generated by binaryTreePaths.

Summary/Discussion

  • Method 1: Recursive Traversal. This method is straightforward and relies on traditional tree traversal practices. However, it can be slow due to its recursive nature.
  • Method 2: String Serialization. This method is easy to implement and benefits from python’s substring functionality. However, large trees may produce long serialization strings, using more memory.
  • Method 3: DFS with Path Recording. It’s a less memory-intensive approach compared to serialization but is potentially less efficient than the recursive approach.
  • Method 4: Utilizing Parent References. This method is efficient for certain tree structures, especially when working with leaf-heavy trees, but requires the binary tree to have parent references, limiting its applicability.
  • Bonus Method 5: Leveraging itertools. This is a highly pythonic and compact solution, leveraging standard libraries, but it can be less clear than other methods and might be inefficient for very large trees due to building all paths upfront.