AlgoMaster Logo

Find Duplicate Subtrees

mediumFrequency11 min readUpdated June 23, 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.

Example Walkthrough

Input:

1243244
root

The array [1, 2, 3, 4, null, 2, 4, null, null, 4] encodes this tree: root 1 has left child 2 and right child 3. Node 2 (left) has a left child 4 and no right child. Node 3 (right) has children 2 and 4, and that inner node 2 has a single left child 4.

collectNodes produces the preorder list of nodes: 1, 2, 4, 3, 2, 4, 4 (the three plain 4 leaves and two [2,4] subtrees).

We then scan each node and count its earlier matches. The leaf 4 appears three times. The first 4 has no earlier match, so its count is 0 and it is skipped. The second 4 matches exactly one earlier node (the first 4), so its count is 1 and we add [4] once. The third 4 matches two earlier nodes, so its count is 2 and it is skipped, which keeps the structure from being reported twice. The subtree rooted at the inner 2 under node 3 matches exactly one earlier node, the left 2 (whose structure is 2 -> left 4), so its count is 1 and we add [2, 4]. Node 1 and node 3 have no earlier twin, so they contribute nothing.

The result holds one [2, 4] subtree and one leaf 4, which prints as [[2,4],[4]].

Output:

2
4
4
result

Code

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.

Example Walkthrough

The tree diagram below shows the order in which postorder DFS visits the nodes of [1, 2, 3, 4, null, 2, 4, null, null, 4], and the second panel tracks the count hash map as each serialization is recorded. A leaf 4 serializes to #,#,4, and the [2,4] subtree serializes to #,#,4,#,2. Each becomes a duplicate the moment its count reaches 2.

1Start postorder DFS: visit leftmost leaf first (node 4 at index 3)
124visit3244
1/7
1Hash map empty, begin postorder traversal
1/7

Node 3 and root 1 each have a unique serialization, so they are never added. The result holds the leaf 4 and the [2,4] subtree, which prints as [[2,4],[4]].

Code

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.

Example Walkthrough

On the same tree [1, 2, 3, 4, null, 2, 4, null, null, 4], the tree panel below labels each node with the ID it receives, and the second panel shows the tuple-to-ID map filling in. Null children carry ID 0, so a leaf 4 forms the tuple (0, 4, 0).

root
1Start postorder DFS, null nodes get ID=0
124visit3244
tupleToId
1Maps empty, null=ID 0, nextId=1
1/7

IDs 1 (the leaf 4) and 2 (the [2,4] subtree) reach count 2, so their nodes are recorded. The result prints as [[2,4],[4]], matching the previous two approaches without ever building a long string.

Code

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.