Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Depth-First Search (DFS)

Builds on Bottom-Up DFS, which you haven’t finished yet.

P2MediumBuilt on Bottom-Up DFS~5 min speedrun

Tree as a String (Serialize and Canonical Key)

Built on
Bottom-Up DFSEach call returns a string built from its children's strings, so equal subtrees give equal strings that can be counted or compared.
The problem

Find Duplicate Subtrees (LC 652)(opens leetcode.com)

The input is one binary tree, passed as its root. A subtree is a node together with all the nodes below it. Two subtrees are duplicates when they have the same shape and the same value at every position.

Some shapes appear more than once in the tree. Find every shape that appears at least twice. For each such shape, report the root of one of its copies, not the root of every copy.

Example 1
Input:root = [1,2,3,4,null,2,4,null,null,4]
Output:[[2,4],[4]]
The node 2 with a left child 4 appears twice, and the single node 4 appears three times. Each shape is reported once. The function on this page returns [2, 4].
4214234

Tree As String

tree ➔ one string per subtree, built from its children's strings ➔ equal subtrees give equal strings, so count, group or compare the strings
Spot it when: The problem asks which subtrees repeat, whether one tree appears inside another, or for a text form of a tree: equal subtrees must give equal strings, so each subtree can be counted, grouped or compared as one string.
Tree As String
Python
1from typing import Callable, List, Optional, Tuple
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9 
10def tree_keys(root: Optional[TreeNode], make_key: Callable[[TreeNode, str, str], str], missing: str = "#") -> List[Tuple[TreeNode, str]]:
11 """Tree as a String: the key of every subtree, in postorder, so the last pair is the
12 root.
13 make_key(node, left_key, right_key) builds the key of node from its value and the
14 keys of its two children. missing is the key of a missing child.
15 Precondition: make_key keeps the position of every part, so two subtrees have the
16 same key exactly when they have the same shape and the same values.
17 N = number of nodes. K = the length of the longest key, which is at most N times the
18 length of one value.
19 Time: O(N * K) | Space: O(N * K)
20 """
21 keys: List[Tuple[TreeNode, str]] = []
22 
23 def solve(node: Optional[TreeNode]) -> str:
24 # 1. Base case: a missing child has the key missing
25 if not node:
26 return missing
27 
28 # 2. Divide: get the key of each child subtree
29 left_key = solve(node.left)
30 right_key = solve(node.right)
31 
32 # 3. Combine: this node's key comes from its value and its two child keys, then
33 # it is recorded
34 key = make_key(node, left_key, right_key)
35 keys.append((node, key))
36 return key
37 
38 solve(root)
39 return keys
Invariant: key_of(node) returns a string that equals the string of another subtree exactly when the two subtrees have the same shape and the same values, because it holds the value, the key of the left child and the key of the right child in fixed positions, with # for a missing child.

Write each subtree as one string, and two subtrees are equal exactly when their strings are equal. key_of(node) gets the key of node.left and the key of node.right first, then joins them with node.val into key. Every key has the same parts in the same order, the value, the left key and the right key, with commas between them and # for a missing child. A position in the string then always means one position in the tree, so two different shapes cannot give the same string. The dictionary seen counts each key, and a shape is reported when its count reaches 2, so one pass finds every repeated shape. The trap is leaving a missing child out of the key. If the key skips left_key or right_key when that child is missing, a node 1 with only a left child 2 and a node 1 with only a right child 2 both give 1,2, and Find Duplicate Subtrees (LC 652) on [0,1,1,2,null,null,2] reports the root value 1 as repeated.

See it run

Tree as a String, step by step

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now 4,#,# · count of this key 1 · answer not yet

key now 2,4,#,#,# · count of this key 1 · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now 4,#,# · count of this key 2 · answer not yet

key now 2,4,#,#,# · count of this key 2 · answer not yet

key now - · count of this key - · answer not yet

key now 4,#,# · count of this key 3 · answer not yet

key now 3,2,4,#,#,#,4,#,# · count of this key 1 · answer not yet

key now 1,2,4,#,#,#,3,2,4,#,#,#,4,#,# · count of this key 1 · answer not yet

key now - · count of this key - · answer [2, 4]

Each circle is a tree node. A node is tagged when its key is built. The keys and their counts are listed under the picture.

  • now the call that has just started
  • waiting a call that waits for the keys of its children
  • keyed a node whose key has been seen once
  • repeat a node whose key has been seen two or more times

Call key_of on the root 1. Each call asks its two children for their keys first, then builds its own key.

Call key_of on node 1. The call waits until both of its children have returned a key.

Call key_of on node 2. The call waits until both of its children have returned a key.

Call key_of on node 4. The call waits until both of its children have returned a key.

Node 4 joins its value with the keys of its two children: 4,#,#. This key is new, so its count is 1.

Node 2 joins its value with the keys of its two children: 2,4,#,#,#. This key is new, so its count is 1.

Call key_of on node 3. The call waits until both of its children have returned a key.

Call key_of on node 2. The call waits until both of its children have returned a key.

Call key_of on node 4. The call waits until both of its children have returned a key.

Node 4 joins its value with the keys of its two children: 4,#,#. This key was seen before, so its count is now 2 and 4 is added to repeated.

Node 2 joins its value with the keys of its two children: 2,4,#,#,#. This key was seen before, so its count is now 2 and 2 is added to repeated.

Call key_of on node 4. The call waits until both of its children have returned a key.

Node 4 joins its value with the keys of its two children: 4,#,#. This key was seen before, so its count is now 3. Nothing more is added.

Node 3 joins its value with the keys of its two children: 3,2,4,#,#,#,4,#,#. This key is new, so its count is 1.

Node 1 joins its value with the keys of its two children: 1,2,4,#,#,#,3,2,4,#,#,#,4,#,#. This key is new, so its count is 1.

The root's call is finished. repeated holds 4, 2, and sorted(repeated) is the answer.

4214234
calls waiting
empty
seen: key x count
empty
repeated (root values)
empty
1 / 16
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Leaving a missing child out of the key

    Mistake
    parts = [str(node.val)]
    if node.left:
        parts.append(left_key)
    if node.right:
        parts.append(right_key)
    key = ",".join(parts)
    Fix
    key = f"{node.val},{left_key},{right_key}"

    What happens: A key that skips a missing child loses the position of the child that is present. A node 1 with only a left child 2 gets the key 1,2. A node 1 with only a right child 2 also gets the key 1,2. The two shapes are different, but they get the same key, so seen counts them as one repeated shape. On [0,1,1,2,null,null,2], the left 1 and the right 1 get the same key, so the root value 1 is reported although the two subtrees are different.

    Example: Find Duplicate Subtrees (LC 652), root = [0,1,1,2,null,null,2]
    Mistake: returns [1, 2]Fix: returns [2]
  2. 2

    Gotcha 2: Joining the parts of the key without separators

    Mistake
    key = f"{node.val}{left_key}{right_key}"
    Fix
    key = f"{node.val},{left_key},{right_key}"

    What happens: Without commas, the digits of neighbouring values run together. A node 1 with a left child 11 gets the key 111###. A node 11 with a left child 1 gets the same key 111###. The two shapes are different, but seen counts them as one repeated shape. On [0,1,11,11,null,1], no shape repeats, but the root value 11 is reported.

    Example: Find Duplicate Subtrees (LC 652), root = [0,1,11,11,null,1]
    Mistake: returns [11]Fix: returns []
  3. 3

    Gotcha 3: Reporting a shape every time it is seen again

    Mistake
    if seen[key] >= 2:
    Fix
    if seen[key] == 2:

    What happens: A shape that appears three times is reached three times. With >= 2, the second sighting and the third sighting both add the root value to repeated, so the answer holds the same shape more than once. LeetCode needs one root for each shape. With == 2, only the second sighting adds a value. On a full tree of 7 nodes with four leaves 3, the leaf shape is reported three times instead of once.

    Example: root = [1,2,2,3,3,3,3]
    Mistake: returns [2, 3, 3, 3]Fix: returns [2, 3]
Not this one if…
  • Each subtree needs only a yes or a number, as in Count Univalue Subtrees (LC 250): use Bottom-Up DFS and return that value from each call. Build a string only when the subtrees of one tree must be compared with each other. See Bottom-Up DFS
  • Two trees are given and the question is whether they match, as in Same Tree (LC 100): use Two Tree Walk, which compares the two trees node by node and builds no string. Build a string key when the subtrees of one tree must be compared with each other. See Two Tree Walk

Deep Dive

Why equal strings mean equal subtrees

key_of(node) is defined by one sentence: it returns a string that describes the subtree at node. The code only has to keep that sentence true for one call, and the sentence then holds for every call.

A missing child returns #. For a real node, left_key and right_key are the true descriptions of its two subtrees, by the same sentence. The key of the node is its value, then left_key, then right_key, with commas between them. So the key lists the nodes in preorder, and it lists a # for every missing child.

Two different subtrees cannot get the same key. Read a key from the left. The first part is the value of the root. The next parts describe the left subtree, and they end exactly where that subtree ends, because every real node is followed by two keys and every # ends a branch. What remains describes the right subtree. So a key can be read in only one way, and it describes one tree.

Take root = [1, 2, 3, 4, null, 2, 4, null, null, 4]. The three leaves 4 each get the key 4,#,#. The two nodes 2 each have one left child 4, so both get the key 2,4,#,#,#. The count of 4,#,# reaches 2 at the second leaf, and the count of 2,4,#,#,# reaches 2 at the second node 2. So repeated becomes [4, 2], and sorted(repeated) is [2, 4].

The walk makes 13 calls: 6 calls on real nodes and 7 calls on missing children.

The trap: a key that leaves out the missing child

Suppose the key skips a child that is missing. A node 1 with only a left child 2 gets the key 1,2. A node 1 with only a right child 2 also gets the key 1,2. These are different shapes with the same key.

On root = [0, 1, 1, 2, null, null, 2], the left 1 and the right 1 get the same key, so 1 is reported as repeated. The correct answer is [2], because only the leaf 2 appears twice. The code that skips the missing child returns [1, 2].

The # for a missing child fixes this. It keeps the position of every part: the left 1 gets 1,2,#,#,# and the right 1 gets 1,#,2,#,#.

The separators matter too. Without commas, a node 1 with a left child 11 gets 111###, and a node 11 with a left child 1 gets the same 111###. On root = [0, 1, 11, 11, null, 1] the code without commas reports 11, and the correct answer is [].

A test for your own key: can you write down two different trees that give the same string? If you can, add a marker or a separator until you cannot.

Other problems on this card: the string is the answer, or the string is the lookup

In Find Duplicate Subtrees (LC 652) the strings are keys. They are counted and never printed. In the other problems on this card, the same string is the answer, or it is used for a lookup.

In Serialize and Deserialize Binary Tree (LC 297), the key of the root is the answer. For [1, 2, 3, null, null, 4, 5] it is 1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A # is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree. The tree built from this string gives the same string again.

In Construct String from Binary Tree (LC 606), the marker is a pair of parentheses, and the key is value(left)(right). A missing right child can be dropped. A missing left child must stay as () when a right child exists. For [1, 2, 3, null, 4] the answer is 1(2()(4))(3). The () that stays is the same marker as #: without it, a left child and a right child would look the same.

In Delete Duplicate Folders in System (LC 1948), the key of a folder is built from its subfolders only. It lists each subfolder's name followed by that subfolder's key, with the subfolders sorted by name. The name of the folder itself is not in its key, because two folders with different names can be identical. A non-empty key that appears twice or more marks all of those folders for deletion.

In Subtree of Another Tree (LC 572), build the key of subRoot, and check whether it is the key of any subtree of root.

The cost, and when a string is the wrong tool

A key is as long as its subtree. Joining it and reading it for the dictionary costs time in proportion to its length. The lengths of all keys add up to the sum of the subtree sizes.

On a complete tree of 1023 nodes with every value 0, the keys add up to 37,891 characters. On a chain of 1000 nodes with every value 0, they add up to 2,003,000 characters. In the worst case the sum is N * (N + 1) / 2 subtree nodes, so the time and the space are O(N^2).

When N is large, give each different key a small number instead of a string. The key of a node is then the triple (node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key has a fixed size, so the time and the space are O(N). Two subtrees get the same number exactly when they have the same shape and the same values.

A chain of 5000 nodes is 5000 calls deep, which is past Python's default limit of about 1000 nested calls. Raise the limit with sys.setrecursionlimit, or walk the tree with an explicit stack.

60-Second Pitch

"The question compares subtrees of one tree with each other, so every subtree needs a name that is the same for equal subtrees. I write key_of(node). It asks the left child and the right child for their keys first, then joins the node's value with those two keys into one string, with commas between the parts. A missing child returns #, so every key has a place for both children. It is correct because the string can be read back in only one way, so two subtrees have the same key exactly when they are equal. I count each key in a dictionary, and when a count reaches 2 I record the root value. The time and space are O(N^2) on a chain, and I can replace each string with a number to get O(N). The trap I avoid is leaving out the # for a missing child."

Common Questions

How is Tree as a String different from Bottom-Up DFS?

Bottom-Up DFS returns a number or a yes from each call, and the answer comes from the root. Tree as a String returns a string that describes the whole subtree, so a call can be compared with every other call. The strings are kept in a dictionary, and the answer comes from the counts.

Why does a missing child need a marker?

The marker keeps the position of every part of the key. Without it, a node with only a left child 2 and a node with only a right child 2 both give 1,2, and two different shapes get the same key.

How do you turn a tree into a string and back?

The key of the root is the string, for example 1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A # is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree.

Why report a shape when the count is exactly 2?

LeetCode needs one root for each repeated shape. A shape that appears three times is reached three times, and only the second sighting has the count 2. With >= 2, the third sighting adds the same shape again.

How do you make the walk faster than O(N^2)?

Give each different key a number. The key of a node becomes the triple (node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key then has a fixed size, so the walk costs O(N) time and space.

SWE Reasoning

Spot it

  • The statement asks which subtrees repeat, or whether two parts of a tree are identical.
  • The statement asks for a text form of a tree, or gives one and asks you to rebuild the tree.
  • Two subtrees are equal when they have the same shape and the same values, and you must compare many pairs of them.
  • You can say in one sentence what key_of(node) returns for the subtree at node.
  • Each subtree needs only a yes or a number, with no comparison between subtrees: use Bottom-Up DFS.

The invariant, step by step

  1. Define in one sentence what key_of(node) returns: a string that describes the subtree at node.
  2. The base case gives a missing child the key #, so a missing child still takes a place in the string.
  3. For a real node, the two child calls run first. By the sentence in step 1, each returns a string that describes its subtree.
  4. The key of the node joins its value, the left key and the right key in this fixed order, with separators.
  5. So two subtrees have the same key exactly when they have the same shape and the same values. Counting keys counts shapes.

Traps

  • Leaving out the missing child. A key that skips a missing child gives 1,2 to a node with only a left child 2 and to a node with only a right child 2. On [0,1,1,2,null,null,2] the answer is [1, 2] instead of [2].
  • No separators. 111### stands for both 1 over 11 and 11 over 1. On [0,1,11,11,null,1] the answer is [11] instead of [].
  • Reporting a shape at every sighting. With >= 2, a shape that appears four times is reported three times. Record it when the count is exactly 2.
  • Long keys on a deep tree. The keys add up to O(N^2) characters on a chain. Use numbers for keys when N is large.

Practice

Canonical LeetCode:Find Duplicate Subtrees (LC 652)Serialize and Deserialize Binary Tree (LC 297)Construct String from Binary Tree (LC 606)Delete Duplicate Folders in System (LC 1948)
Theory Context•Depth-First Search (DFS)

Builds on Bottom-Up DFS, which you haven’t finished yet.

P2MediumBuilt on Bottom-Up DFS~5 min speedrun

Tree as a String (Serialize and Canonical Key)

Built on
Bottom-Up DFSEach call returns a string built from its children's strings, so equal subtrees give equal strings that can be counted or compared.
The problem

Find Duplicate Subtrees (LC 652)(opens leetcode.com)

The input is one binary tree, passed as its root. A subtree is a node together with all the nodes below it. Two subtrees are duplicates when they have the same shape and the same value at every position.

Some shapes appear more than once in the tree. Find every shape that appears at least twice. For each such shape, report the root of one of its copies, not the root of every copy.

Example 1
Input:root = [1,2,3,4,null,2,4,null,null,4]
Output:[[2,4],[4]]
The node 2 with a left child 4 appears twice, and the single node 4 appears three times. Each shape is reported once. The function on this page returns [2, 4].
4214234

Tree As String

tree ➔ one string per subtree, built from its children's strings ➔ equal subtrees give equal strings, so count, group or compare the strings
Spot it when: The problem asks which subtrees repeat, whether one tree appears inside another, or for a text form of a tree: equal subtrees must give equal strings, so each subtree can be counted, grouped or compared as one string.
Tree As String
Python
1from typing import Callable, List, Optional, Tuple
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9 
10def tree_keys(root: Optional[TreeNode], make_key: Callable[[TreeNode, str, str], str], missing: str = "#") -> List[Tuple[TreeNode, str]]:
11 """Tree as a String: the key of every subtree, in postorder, so the last pair is the
12 root.
13 make_key(node, left_key, right_key) builds the key of node from its value and the
14 keys of its two children. missing is the key of a missing child.
15 Precondition: make_key keeps the position of every part, so two subtrees have the
16 same key exactly when they have the same shape and the same values.
17 N = number of nodes. K = the length of the longest key, which is at most N times the
18 length of one value.
19 Time: O(N * K) | Space: O(N * K)
20 """
21 keys: List[Tuple[TreeNode, str]] = []
22 
23 def solve(node: Optional[TreeNode]) -> str:
24 # 1. Base case: a missing child has the key missing
25 if not node:
26 return missing
27 
28 # 2. Divide: get the key of each child subtree
29 left_key = solve(node.left)
30 right_key = solve(node.right)
31 
32 # 3. Combine: this node's key comes from its value and its two child keys, then
33 # it is recorded
34 key = make_key(node, left_key, right_key)
35 keys.append((node, key))
36 return key
37 
38 solve(root)
39 return keys
Invariant: key_of(node) returns a string that equals the string of another subtree exactly when the two subtrees have the same shape and the same values, because it holds the value, the key of the left child and the key of the right child in fixed positions, with # for a missing child.

Write each subtree as one string, and two subtrees are equal exactly when their strings are equal. key_of(node) gets the key of node.left and the key of node.right first, then joins them with node.val into key. Every key has the same parts in the same order, the value, the left key and the right key, with commas between them and # for a missing child. A position in the string then always means one position in the tree, so two different shapes cannot give the same string. The dictionary seen counts each key, and a shape is reported when its count reaches 2, so one pass finds every repeated shape. The trap is leaving a missing child out of the key. If the key skips left_key or right_key when that child is missing, a node 1 with only a left child 2 and a node 1 with only a right child 2 both give 1,2, and Find Duplicate Subtrees (LC 652) on [0,1,1,2,null,null,2] reports the root value 1 as repeated.

See it run

Tree as a String, step by step

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now 4,#,# · count of this key 1 · answer not yet

key now 2,4,#,#,# · count of this key 1 · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now - · count of this key - · answer not yet

key now 4,#,# · count of this key 2 · answer not yet

key now 2,4,#,#,# · count of this key 2 · answer not yet

key now - · count of this key - · answer not yet

key now 4,#,# · count of this key 3 · answer not yet

key now 3,2,4,#,#,#,4,#,# · count of this key 1 · answer not yet

key now 1,2,4,#,#,#,3,2,4,#,#,#,4,#,# · count of this key 1 · answer not yet

key now - · count of this key - · answer [2, 4]

Each circle is a tree node. A node is tagged when its key is built. The keys and their counts are listed under the picture.

  • now the call that has just started
  • waiting a call that waits for the keys of its children
  • keyed a node whose key has been seen once
  • repeat a node whose key has been seen two or more times

Call key_of on the root 1. Each call asks its two children for their keys first, then builds its own key.

Call key_of on node 1. The call waits until both of its children have returned a key.

Call key_of on node 2. The call waits until both of its children have returned a key.

Call key_of on node 4. The call waits until both of its children have returned a key.

Node 4 joins its value with the keys of its two children: 4,#,#. This key is new, so its count is 1.

Node 2 joins its value with the keys of its two children: 2,4,#,#,#. This key is new, so its count is 1.

Call key_of on node 3. The call waits until both of its children have returned a key.

Call key_of on node 2. The call waits until both of its children have returned a key.

Call key_of on node 4. The call waits until both of its children have returned a key.

Node 4 joins its value with the keys of its two children: 4,#,#. This key was seen before, so its count is now 2 and 4 is added to repeated.

Node 2 joins its value with the keys of its two children: 2,4,#,#,#. This key was seen before, so its count is now 2 and 2 is added to repeated.

Call key_of on node 4. The call waits until both of its children have returned a key.

Node 4 joins its value with the keys of its two children: 4,#,#. This key was seen before, so its count is now 3. Nothing more is added.

Node 3 joins its value with the keys of its two children: 3,2,4,#,#,#,4,#,#. This key is new, so its count is 1.

Node 1 joins its value with the keys of its two children: 1,2,4,#,#,#,3,2,4,#,#,#,4,#,#. This key is new, so its count is 1.

The root's call is finished. repeated holds 4, 2, and sorted(repeated) is the answer.

4214234
calls waiting
empty
seen: key x count
empty
repeated (root values)
empty
1 / 16
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Leaving a missing child out of the key

    Mistake
    parts = [str(node.val)]
    if node.left:
        parts.append(left_key)
    if node.right:
        parts.append(right_key)
    key = ",".join(parts)
    Fix
    key = f"{node.val},{left_key},{right_key}"

    What happens: A key that skips a missing child loses the position of the child that is present. A node 1 with only a left child 2 gets the key 1,2. A node 1 with only a right child 2 also gets the key 1,2. The two shapes are different, but they get the same key, so seen counts them as one repeated shape. On [0,1,1,2,null,null,2], the left 1 and the right 1 get the same key, so the root value 1 is reported although the two subtrees are different.

    Example: Find Duplicate Subtrees (LC 652), root = [0,1,1,2,null,null,2]
    Mistake: returns [1, 2]Fix: returns [2]
  2. 2

    Gotcha 2: Joining the parts of the key without separators

    Mistake
    key = f"{node.val}{left_key}{right_key}"
    Fix
    key = f"{node.val},{left_key},{right_key}"

    What happens: Without commas, the digits of neighbouring values run together. A node 1 with a left child 11 gets the key 111###. A node 11 with a left child 1 gets the same key 111###. The two shapes are different, but seen counts them as one repeated shape. On [0,1,11,11,null,1], no shape repeats, but the root value 11 is reported.

    Example: Find Duplicate Subtrees (LC 652), root = [0,1,11,11,null,1]
    Mistake: returns [11]Fix: returns []
  3. 3

    Gotcha 3: Reporting a shape every time it is seen again

    Mistake
    if seen[key] >= 2:
    Fix
    if seen[key] == 2:

    What happens: A shape that appears three times is reached three times. With >= 2, the second sighting and the third sighting both add the root value to repeated, so the answer holds the same shape more than once. LeetCode needs one root for each shape. With == 2, only the second sighting adds a value. On a full tree of 7 nodes with four leaves 3, the leaf shape is reported three times instead of once.

    Example: root = [1,2,2,3,3,3,3]
    Mistake: returns [2, 3, 3, 3]Fix: returns [2, 3]
Not this one if…
  • Each subtree needs only a yes or a number, as in Count Univalue Subtrees (LC 250): use Bottom-Up DFS and return that value from each call. Build a string only when the subtrees of one tree must be compared with each other. See Bottom-Up DFS
  • Two trees are given and the question is whether they match, as in Same Tree (LC 100): use Two Tree Walk, which compares the two trees node by node and builds no string. Build a string key when the subtrees of one tree must be compared with each other. See Two Tree Walk

Deep Dive

Why equal strings mean equal subtrees

key_of(node) is defined by one sentence: it returns a string that describes the subtree at node. The code only has to keep that sentence true for one call, and the sentence then holds for every call.

A missing child returns #. For a real node, left_key and right_key are the true descriptions of its two subtrees, by the same sentence. The key of the node is its value, then left_key, then right_key, with commas between them. So the key lists the nodes in preorder, and it lists a # for every missing child.

Two different subtrees cannot get the same key. Read a key from the left. The first part is the value of the root. The next parts describe the left subtree, and they end exactly where that subtree ends, because every real node is followed by two keys and every # ends a branch. What remains describes the right subtree. So a key can be read in only one way, and it describes one tree.

Take root = [1, 2, 3, 4, null, 2, 4, null, null, 4]. The three leaves 4 each get the key 4,#,#. The two nodes 2 each have one left child 4, so both get the key 2,4,#,#,#. The count of 4,#,# reaches 2 at the second leaf, and the count of 2,4,#,#,# reaches 2 at the second node 2. So repeated becomes [4, 2], and sorted(repeated) is [2, 4].

The walk makes 13 calls: 6 calls on real nodes and 7 calls on missing children.

The trap: a key that leaves out the missing child

Suppose the key skips a child that is missing. A node 1 with only a left child 2 gets the key 1,2. A node 1 with only a right child 2 also gets the key 1,2. These are different shapes with the same key.

On root = [0, 1, 1, 2, null, null, 2], the left 1 and the right 1 get the same key, so 1 is reported as repeated. The correct answer is [2], because only the leaf 2 appears twice. The code that skips the missing child returns [1, 2].

The # for a missing child fixes this. It keeps the position of every part: the left 1 gets 1,2,#,#,# and the right 1 gets 1,#,2,#,#.

The separators matter too. Without commas, a node 1 with a left child 11 gets 111###, and a node 11 with a left child 1 gets the same 111###. On root = [0, 1, 11, 11, null, 1] the code without commas reports 11, and the correct answer is [].

A test for your own key: can you write down two different trees that give the same string? If you can, add a marker or a separator until you cannot.

Other problems on this card: the string is the answer, or the string is the lookup

In Find Duplicate Subtrees (LC 652) the strings are keys. They are counted and never printed. In the other problems on this card, the same string is the answer, or it is used for a lookup.

In Serialize and Deserialize Binary Tree (LC 297), the key of the root is the answer. For [1, 2, 3, null, null, 4, 5] it is 1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A # is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree. The tree built from this string gives the same string again.

In Construct String from Binary Tree (LC 606), the marker is a pair of parentheses, and the key is value(left)(right). A missing right child can be dropped. A missing left child must stay as () when a right child exists. For [1, 2, 3, null, 4] the answer is 1(2()(4))(3). The () that stays is the same marker as #: without it, a left child and a right child would look the same.

In Delete Duplicate Folders in System (LC 1948), the key of a folder is built from its subfolders only. It lists each subfolder's name followed by that subfolder's key, with the subfolders sorted by name. The name of the folder itself is not in its key, because two folders with different names can be identical. A non-empty key that appears twice or more marks all of those folders for deletion.

In Subtree of Another Tree (LC 572), build the key of subRoot, and check whether it is the key of any subtree of root.

The cost, and when a string is the wrong tool

A key is as long as its subtree. Joining it and reading it for the dictionary costs time in proportion to its length. The lengths of all keys add up to the sum of the subtree sizes.

On a complete tree of 1023 nodes with every value 0, the keys add up to 37,891 characters. On a chain of 1000 nodes with every value 0, they add up to 2,003,000 characters. In the worst case the sum is N * (N + 1) / 2 subtree nodes, so the time and the space are O(N^2).

When N is large, give each different key a small number instead of a string. The key of a node is then the triple (node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key has a fixed size, so the time and the space are O(N). Two subtrees get the same number exactly when they have the same shape and the same values.

A chain of 5000 nodes is 5000 calls deep, which is past Python's default limit of about 1000 nested calls. Raise the limit with sys.setrecursionlimit, or walk the tree with an explicit stack.

60-Second Pitch

"The question compares subtrees of one tree with each other, so every subtree needs a name that is the same for equal subtrees. I write key_of(node). It asks the left child and the right child for their keys first, then joins the node's value with those two keys into one string, with commas between the parts. A missing child returns #, so every key has a place for both children. It is correct because the string can be read back in only one way, so two subtrees have the same key exactly when they are equal. I count each key in a dictionary, and when a count reaches 2 I record the root value. The time and space are O(N^2) on a chain, and I can replace each string with a number to get O(N). The trap I avoid is leaving out the # for a missing child."

Common Questions

How is Tree as a String different from Bottom-Up DFS?

Bottom-Up DFS returns a number or a yes from each call, and the answer comes from the root. Tree as a String returns a string that describes the whole subtree, so a call can be compared with every other call. The strings are kept in a dictionary, and the answer comes from the counts.

Why does a missing child need a marker?

The marker keeps the position of every part of the key. Without it, a node with only a left child 2 and a node with only a right child 2 both give 1,2, and two different shapes get the same key.

How do you turn a tree into a string and back?

The key of the root is the string, for example 1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A # is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree.

Why report a shape when the count is exactly 2?

LeetCode needs one root for each repeated shape. A shape that appears three times is reached three times, and only the second sighting has the count 2. With >= 2, the third sighting adds the same shape again.

How do you make the walk faster than O(N^2)?

Give each different key a number. The key of a node becomes the triple (node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key then has a fixed size, so the walk costs O(N) time and space.

SWE Reasoning

Spot it

  • The statement asks which subtrees repeat, or whether two parts of a tree are identical.
  • The statement asks for a text form of a tree, or gives one and asks you to rebuild the tree.
  • Two subtrees are equal when they have the same shape and the same values, and you must compare many pairs of them.
  • You can say in one sentence what key_of(node) returns for the subtree at node.
  • Each subtree needs only a yes or a number, with no comparison between subtrees: use Bottom-Up DFS.

The invariant, step by step

  1. Define in one sentence what key_of(node) returns: a string that describes the subtree at node.
  2. The base case gives a missing child the key #, so a missing child still takes a place in the string.
  3. For a real node, the two child calls run first. By the sentence in step 1, each returns a string that describes its subtree.
  4. The key of the node joins its value, the left key and the right key in this fixed order, with separators.
  5. So two subtrees have the same key exactly when they have the same shape and the same values. Counting keys counts shapes.

Traps

  • Leaving out the missing child. A key that skips a missing child gives 1,2 to a node with only a left child 2 and to a node with only a right child 2. On [0,1,1,2,null,null,2] the answer is [1, 2] instead of [2].
  • No separators. 111### stands for both 1 over 11 and 11 over 1. On [0,1,11,11,null,1] the answer is [11] instead of [].
  • Reporting a shape at every sighting. With >= 2, a shape that appears four times is reported three times. Record it when the count is exactly 2.
  • Long keys on a deep tree. The keys add up to O(N^2) characters on a chain. Use numbers for keys when N is large.

Practice

Canonical LeetCode:Find Duplicate Subtrees (LC 652)Serialize and Deserialize Binary Tree (LC 297)Construct String from Binary Tree (LC 606)Delete Duplicate Folders in System (LC 1948)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•