Find All Duplicate Subtrees in a Binary Tree
Problem and examples
| Input Tree | Duplicate Subtrees (Serialized) | Description |
|---|---|---|
| [1, 2, 3, 4, null, 2, 4, null, null, 4] | ['4', '2,4,null,null,null'] | The subtree rooted at value 4 appears three times. The subtree [2, 4] also appears twice. |
| [1, 2, 3, 4, null, 4, null, null, null, 4] | ['4'] | The leaf node with value 4 appears multiple times, forming duplicate subtrees. |
| [1, 2, 3] | [] | All subtrees are unique; no duplicate subtree structure exists. |
| [0, 0, 0, 0, null, null, 0] | ['0'] | The leaf node with value 0 appears three times. These are duplicate subtrees. |
| [2, 1, 1, null, null, null, 1] | ['1'] | There are multiple leaf nodes with value 1, which are considered duplicate subtrees. |
How it works
Understanding the Problem
We are given a binary tree, and our goal is to find all duplicate subtrees. A duplicate subtree is one where both the structure and node values are identical to another subtree in the tree. Each duplicate should be returned only once, represented by its root node.
To identify duplicates, we must compare every subtree with all others. But doing this naively would be inefficient. Instead, we use a serialization strategy to transform each subtree into a string format, allowing quick lookup and comparison using a hash map.
Step-by-Step Solution with Example
Step 1: Visualize the Example
Let’s take the input tree: [1,2,3,4,null,2,4,null,null,4]
This corresponds to the following structure:
1
/ 2 3
/ / 4 2 4
/
4
We can observe:
- Three nodes with value 4, all as leaf nodes.
- Two subtrees with structure
2 → 4.
Step 2: Define Serialization Format
We perform a post-order traversal (left → right → node) and serialize each subtree into a string.
For example:
- Leaf node 4 becomes
"4,#,#" - Subtree 2 → 4 becomes
"2,4,#,#,#"
Step 3: Track and Detect Duplicates
We maintain a hash map where keys are serialization strings and values are their frequency.
As we serialize each subtree:
- If a serialization is seen once, we add the subtree root to the result.
- If it’s already seen more than once, we skip it to avoid duplicates in the result.
Step 4: Return the Result
From our example:
- Serialization
"4,#,#"appears 3 times → add one root node of 4 - Serialization
"2,4,#,#,#"appears 2 times → add one root node of 2
Edge Cases
Case 1: All Subtrees Are Unique
Input: [1,2,3]
Structure:
1 / 2 3Each node is unique. Serialized strings for each subtree do not match. Result:
[].
Case 2: Empty Tree
Input: []
There are no nodes in the tree. Hence, no subtrees exist. Result: [].
Case 3: Large Tree with Many Duplicates
Ensure that your solution uses efficient traversal and hashing to handle such cases within time limits.
Finally
This problem is a great example of using serialization to simplify complex structure comparisons. It teaches you how to:
- Break down trees into reusable string representations
- Use hash maps to count and identify duplicates efficiently
- Handle edge cases like null roots and small trees gracefully
- Perform a postorder traversal of the binary tree.
- For each node, generate a unique serialization string representing the subtree rooted at that node. Use a format like
node.val,left_serial,right_serial. - Use a hash map (or dictionary) to count the occurrences of each serialized subtree.
- If a serialization appears more than once, add the subtree's root to the result list (ensuring each duplicate is added only once).
- Return the list of duplicate subtree roots.
Implementation and efficiency
Python implementation
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def findDuplicateSubtrees(self, root: TreeNode) -> list:
count = {}
result = []
def traverse(node):
if not node:
return '#'
serial = f'{node.val},' + traverse(node.left) + ',' + traverse(node.right)
count[serial] = count.get(serial, 0) + 1
if count[serial] == 2:
result.append(node)
return serial
traverse(root)
return result
# Example usage:
if __name__ == '__main__':
# Construct binary tree sample
# 1
# / \
# 2 3
# / / \
# 4 2 4
# /
# 4
root = TreeNode(1, TreeNode(2, TreeNode(4)), TreeNode(3, TreeNode(2, TreeNode(4)), TreeNode(4)))
sol = Solution()
duplicates = sol.findDuplicateSubtrees(root)
for node in duplicates:
print(node.val)Java implementation
import java.util.*;
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int x) { val = x; }
}
public class Solution {
Map<String, Integer> count = new HashMap<>();
List<TreeNode> result = new ArrayList<>();
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
serialize(root);
return result;
}
private String serialize(TreeNode node) {
if (node == null) return "#";
String serial = node.val + "," + serialize(node.left) + "," + serialize(node.right);
count.put(serial, count.getOrDefault(serial, 0) + 1);
if (count.get(serial) == 2) result.add(node);
return serial;
}
public static void main(String[] args) {
// Construct sample tree similar to the Python example
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.right.left = new TreeNode(2);
root.right.left.left = new TreeNode(4);
root.right.right = new TreeNode(4);
Solution sol = new Solution();
List<TreeNode> duplicates = sol.findDuplicateSubtrees(root);
for (TreeNode node : duplicates) {
System.out.println(node.val);
}
}
}JavaScript implementation
class TreeNode {
constructor(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function findDuplicateSubtrees(root) {
const count = new Map();
const result = [];
function serialize(node) {
if (!node) return '#';
const serial = `${node.val},${serialize(node.left)},${serialize(node.right)}`;
count.set(serial, (count.get(serial) || 0) + 1);
if (count.get(serial) === 2) result.push(node);
return serial;
}
serialize(root);
return result;
}
// Example usage:
const root = new TreeNode(1,
new TreeNode(2, new TreeNode(4)),
new TreeNode(3, new TreeNode(2, new TreeNode(4)), new TreeNode(4))
);
console.log(findDuplicateSubtrees(root));C implementation
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Note: Implementing a full hash map in C is complex. The following is a simplified pseudo-implementation
// for educational purposes and may not handle all edge cases or memory management.
#define TABLE_SIZE 1000
typedef struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// A simple structure for hash map entries
typedef struct {
char *key;
int count;
TreeNode *node; // store the first duplicate occurrence
} HashEntry;
HashEntry *hashTable[TABLE_SIZE];
unsigned int hash(const char *str) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash % TABLE_SIZE;
}
char* serialize(TreeNode *node) {
if (!node) return strdup("#");
char leftSerial[256], rightSerial[256];
char *left = serialize(node->left);
char *right = serialize(node->right);
snprintf(leftSerial, sizeof(leftSerial), "%s", left);
snprintf(rightSerial, sizeof(rightSerial), "%s", right);
free(left);
free(right);
char *serial = malloc(256);
snprintf(serial, 256, "%d,%s,%s", node->val, leftSerial, rightSerial);
return serial;
}
// Global result array (for simplicity, fixed size)
TreeNode **result;
int resultCount = 0;
void addToHash(const char *serial, TreeNode *node) {
unsigned int index = hash(serial);
// linear probing for simplicity
while (hashTable[index] != NULL) {
if (strcmp(hashTable[index]->key, serial) == 0) {
hashTable[index]->count++;
if (hashTable[index]->count == 2) {
result[resultCount++] = node;
}
return;
}
index = (index + 1) % TABLE_SIZE;
}
HashEntry *entry = malloc(sizeof(HashEntry));
entry->key = strdup(serial);
entry->count = 1;
entry->node = node;
hashTable[index] = entry;
}
char* traverse(TreeNode *node) {
if (!node) return strdup("#");
char *leftSerial = traverse(node->left);
char *rightSerial = traverse(node->right);
char *serial = malloc(256);
snprintf(serial, 256, "%d,%s,%s", node->val, leftSerial, rightSerial);
addToHash(serial, node);
free(leftSerial);
free(rightSerial);
return serial;
}
// Example usage:
int main() {
// Construct tree:
// 1
// / \
// 2 3
// / / \
// 4 2 4
// /
// 4
TreeNode n4a = {4, NULL, NULL};
TreeNode n2a = {2, &n4a, NULL};
TreeNode n4b = {4, NULL, NULL};
TreeNode n2b = {2, &n4b, NULL};
TreeNode n4c = {4, NULL, NULL};
TreeNode n3 = {3, &n2b, &n4c};
TreeNode root = {1, &n2a, &n3};
result = malloc(100 * sizeof(TreeNode*));
traverse(&root);
printf("Duplicate subtree roots (by value):\n");
for (int i = 0; i < resultCount; i++) {
printf("%d\n", result[i]->val);
}
// Free resources omitted for brevity
return 0;
}
C++ implementation
#include <iostream>
#include <unordered_map>
#include <vector>
#include <string>
using namespace std;
struct TreeNode {
int val;
TreeNode *left, *right;
TreeNode(int x): val(x), left(nullptr), right(nullptr) {}
};
class Solution {
unordered_map<string, int> count;
vector<TreeNode*> result;
string serialize(TreeNode* node) {
if (!node) return "#";
string serial = to_string(node->val) + "," + serialize(node->left) + "," + serialize(node->right);
count[serial]++;
if (count[serial] == 2) result.push_back(node);
return serial;
}
public:
vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
serialize(root);
return result;
}
};
int main() {
// Construct sample tree
TreeNode* n4a = new TreeNode(4);
TreeNode* n2a = new TreeNode(2);
n2a->left = n4a;
TreeNode* n4b = new TreeNode(4);
TreeNode* n2b = new TreeNode(2);
n2b->left = n4b;
TreeNode* n4c = new TreeNode(4);
TreeNode* n3 = new TreeNode(3);
n3->left = n2b;
n3->right = n4c;
TreeNode* root = new TreeNode(1);
root->left = n2a;
root->right = n3;
Solution sol;
vector<TreeNode*> duplicates = sol.findDuplicateSubtrees(root);
for (auto node : duplicates) {
cout << node->val << "\n";
}
return 0;
}
Go implementation
package main
import (
"fmt"
"strconv"
)
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func findDuplicateSubtrees(root *TreeNode) []*TreeNode {
count := make(map[string]int)
var result []*TreeNode
var serialize func(*TreeNode) string
serialize = func(node *TreeNode) string {
if node == nil {
return "#"
}
serial := strconv.Itoa(node.Val) + "," + serialize(node.Left) + "," + serialize(node.Right)
count[serial]++
if count[serial] == 2 {
result = append(result, node)
}
return serial
}
serialize(root)
return result
}
func main() {
root := &TreeNode{1,
&TreeNode{2, &TreeNode{4, nil, nil}, nil},
&TreeNode{3, &TreeNode{2, &TreeNode{4, nil, nil}, nil}, &TreeNode{4, nil, nil}},
}
duplicates := findDuplicateSubtrees(root)
fmt.Println("Duplicate subtree roots (by value):")
for _, node := range duplicates {
fmt.Println(node.Val)
}
}Rust implementation
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Clone)]
struct TreeNode {
val: i32,
left: Option<Box<TreeNode>>,
right: Option<Box<TreeNode>>,
}
impl TreeNode {
fn new(val: i32) -> Self {
TreeNode { val, left: None, right: None }
}
}
fn find_duplicate_subtrees(root: Option<&Box<TreeNode>>) -> Vec<i32> {
let mut count = HashMap::new();
let mut result = vec![];
fn serialize(node: Option<&Box<TreeNode>>, count: &mut HashMap<String, usize>, result: &mut Vec<i32>) -> String {
if let Some(n) = node {
let serial = format!("{}, {}, {}", n.val,
serialize(n.left.as_ref(), count, result),
serialize(n.right.as_ref(), count, result));
let entry = count.entry(serial.clone()).or_insert(0);
*entry += 1;
if *entry == 2 {
result.push(n.val);
}
serial
} else {
"#".to_string()
}
}
serialize(root, &mut count, &mut result);
result
}
fn main() {
let tree = Box::new(TreeNode {
val: 1,
left: Some(Box::new(TreeNode {
val: 2,
left: Some(Box::new(TreeNode::new(4))),
right: None,
})),
right: Some(Box::new(TreeNode {
val: 3,
left: Some(Box::new(TreeNode {
val: 2,
left: Some(Box::new(TreeNode::new(4))),
right: None,
})),
right: Some(Box::new(TreeNode::new(4))),
})),
});
let duplicates = find_duplicate_subtrees(Some(&tree));
println!("Duplicate subtree roots (by value): {:?}", duplicates);
}Kotlin implementation
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
}
class Solution {
private val count = mutableMapOf<String, Int>()
private val result = mutableListOf<TreeNode>()
fun findDuplicateSubtrees(root: TreeNode?): List<TreeNode> {
serialize(root)
return result
}
private fun serialize(node: TreeNode?): String {
if (node == null) return "#"
val serial = "${node.`val`}," + serialize(node.left) + "," + serialize(node.right)
count[serial] = count.getOrDefault(serial, 0) + 1
if (count[serial] == 2) result.add(node)
return serial
}
}
fun main() {
val root = TreeNode(1)
root.left = TreeNode(2).apply { left = TreeNode(4) }
root.right = TreeNode(3).apply {
left = TreeNode(2).apply { left = TreeNode(4) }
right = TreeNode(4)
}
val sol = Solution()
val duplicates = sol.findDuplicateSubtrees(root)
println("Duplicate subtree roots (by value):")
duplicates.forEach { println(it.`val`) }
}Swift implementation
import Foundation
class TreeNode {
var val: Int
var left: TreeNode?
var right: TreeNode?
init(_ val: Int) {
self.val = val
}
}
class Solution {
var count = [String: Int]()
var result = [TreeNode]()
func findDuplicateSubtrees(_ root: TreeNode?) -> [TreeNode] {
_ = serialize(root)
return result
}
func serialize(_ node: TreeNode?) -> String {
guard let node = node else { return "#" }
let serial = "\(node.val),\(serialize(node.left)),\(serialize(node.right))"
count[serial, default: 0] += 1
if count[serial] == 2 {
result.append(node)
}
return serial
}
}
// Example usage
let root = TreeNode(1)
root.left = TreeNode(2)
root.left?.left = TreeNode(4)
root.right = TreeNode(3)
root.right?.left = TreeNode(2)
root.right?.left?.left = TreeNode(4)
root.right?.right = TreeNode(4)
let solution = Solution()
let duplicates = solution.findDuplicateSubtrees(root)
print("Duplicate subtree roots (by value):")
for node in duplicates {
print(node.val)
}TypeScript implementation
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function findDuplicateSubtrees(root: TreeNode | null): TreeNode[] {
const count = new Map<string, number>();
const result: TreeNode[] = [];
function serialize(node: TreeNode | null): string {
if (!node) return '#';
const serial = `${node.val},${serialize(node.left)},${serialize(node.right)}`;
count.set(serial, (count.get(serial) || 0) + 1);
if (count.get(serial) === 2) {
result.push(node);
}
return serial;
}
serialize(root);
return result;
}
// Example usage:
const root = new TreeNode(1,
new TreeNode(2, new TreeNode(4)),
new TreeNode(3, new TreeNode(2, new TreeNode(4)), new TreeNode(4))
);
const duplicates = findDuplicateSubtrees(root);
console.log("Duplicate subtree roots (by value):");
duplicates.forEach(node => console.log(node.val));