AlgoMaster Logo

Find Duplicate Subtrees

mediumFrequencyUpdated September 3, 2026

Understanding the Problem

We are given a binary tree, and we need to find all subtrees that appear more than once. A subtree here means a node along with all of its descendants, preserving structure. Two subtrees are considered duplicates only if they have the exact same shape and the same values at every position.

The challenge is comparing subtrees without doing redundant work. Comparing every pair of subtrees directly is quadratic in the number of nodes, and each comparison can itself cost linear time. A better route is to represent each subtree as a string (a serialization) so that two subtrees are duplicates if and only if their serializations match. That turns "compare tree structures" into "find duplicate strings," which a hash map handles in constant time per lookup.

Key Constraints:

  • 1 <= number of nodes <= 5000 -- An O(n^2) approach is feasible at this size, but the serialization strings themselves can grow to O(n) characters on a skewed tree, so a naive string approach degrades to O(n^2) total work. The ID-based approach below avoids that.
  • -200 <= Node.val <= 200 -- Values can be negative, so the serialization has to keep the sign and a delimiter. Writing the raw digits with no separator would let different trees collide, which the next sections make precise.
  • At least 1 node -- There is no empty-tree edge case to handle.

Approach 1: Brute Force (Compare All Pairs)

Intuition

For every node in the tree, check whether the subtree rooted at it is identical to the subtree rooted at some earlier node. The first time a structure shows up again, it is a duplicate and we record it once.

To check whether two subtrees are identical, compare them recursively: the roots hold the same value, the left subtrees match, and the right subtrees match.

The challenge is reporting each duplicate structure exactly once even when it appears three or more times. We solve that by counting how many earlier nodes match the current one. A node is the second occurrence of its structure precisely when exactly one earlier node matches it, so we add it then and never again.

This does a lot of repeated work. There are O(n^2) pairs of nodes, and each comparison can visit O(n) nodes in the worst case, giving an O(n^3) running time.

Algorithm

  1. Collect all nodes in the tree into a list using any traversal.
  2. For each node i, count how many earlier nodes (indices 0 to i-1) root a subtree identical to the one at node i.
  3. To check identity, recursively compare the two subtrees: same value, same left subtrees, same right subtrees.
  4. If that count is exactly 1, node i is the second occurrence of its structure, so add it to the result. A third or later occurrence has a count of 2 or more, so it is skipped and never duplicated.
  5. Return the result list.

Visualization and Code

Loading animation...

The cost comes from comparing every pair of subtrees from scratch. The next approach builds a compact fingerprint for each subtree once, so comparison becomes a hash map lookup.

Approach 2: Postorder Serialization with Hash Map

Intuition

Instead of comparing subtrees structurally, serialize each subtree into a string. Two subtrees are identical if and only if their serializations match, which turns the tree comparison into string matching.

We build the serialization with a postorder traversal (left, right, node) so it grows bottom-up. For each node, we take the serialization of its left subtree, the serialization of its right subtree, and the current node's value, then join them. A null child becomes a marker like #.

Each serialization goes into a hash map keyed by the string, with a count of occurrences. The first time a string appears its count is 1; the second time it reaches 2, which is when we record the current node as a duplicate. Later occurrences are ignored, so each duplicate structure is added exactly once.

The delimiter between fields is load-bearing, not decoration. Consider serializing a node with value 1 whose only child is a leaf with value 2, versus a single leaf with value 12. Without separators, the first could read as 122 (something like value 1, child 2, plus null markers) and a different tree could produce the same run of characters, so two distinct trees would collide and be reported as duplicates. The value range here makes this concrete: values can be negative and multi-digit, so 2 followed by 1 must not be confused with 21. Writing the comma between every field, and a distinct # for null, guarantees the string parses back to exactly one tree, so equal strings mean equal trees.

Algorithm

  1. Initialize a hash map to count serialization occurrences and a result list.
  2. Perform a postorder DFS on the tree.
  3. At each node, build the serialization: leftSerial + "," + rightSerial + "," + node.val.
  4. For null nodes, return "#" as the serialization.
  5. Increment the count for this serialization in the hash map.
  6. If the count becomes exactly 2, add the current node to the result list.
  7. Return the serialization string so the parent can use it.
  8. After the DFS completes, return the result list.

Visualization and Code

Loading animation...

The cost comes from the string serialization itself. On a skewed tree, the total string data across all nodes reaches O(n^2). The next approach replaces each long string with a short, fixed-size identifier.

Approach 3: ID-Based Serialization (Optimal)

Intuition

The string approach pays for long serializations because a parent's string contains the full serialization of both children, which contains the full serialization of the grandchildren, and so on. Each subtree's representation copies everything below it.

Instead, assign each distinct subtree structure a numeric ID. A subtree is then described by three numbers: the left child's ID, the current node's value, and the right child's ID. This is a fixed-size tuple regardless of subtree depth, the same way a reference to a document by its page number is fixed-size no matter how long the document is. Two subtrees are identical exactly when they map to the same tuple, so the same tuple maps to the same ID. The ID of a subtree built from those three numbers is shared across every occurrence of that structure, which is why counting IDs is enough to detect duplicates.

Algorithm

  1. Initialize a map from tuple (leftID, nodeVal, rightID) to a unique integer ID, starting IDs from 1. Reserve ID 0 for null nodes.
  2. Initialize a count map from ID to occurrence count, and a result list.
  3. Perform a postorder DFS.
  4. For null nodes, return ID 0.
  5. At each node, recursively get the left child's ID and right child's ID.
  6. Form the tuple (leftID, node.val, rightID).
  7. If the tuple is not in the map, assign it a new ID.
  8. Look up the ID for this tuple and increment its count.
  9. If the count equals 2, add the current node to the result.
  10. Return the ID for this subtree.

Visualization and Code

Loading animation...

A related variant replaces the integer IDs with a hash computed from the children's hashes and the node value, a Merkle-style hash-of-children scheme. It keeps the O(n) running time and constant-size keys without maintaining a separate ID counter, at the cost of handling hash collisions (two different subtrees mapping to the same hash). The ID-based approach sidesteps collisions entirely because distinct tuples always get distinct IDs, so it is the safer default for an exact-match requirement.