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•Stack

Builds on Digit Manipulation, which you haven’t finished yet.

P2MediumMental model~4 min speedrun

Length-Prefix Encoding

The problem

Encode and Decode Strings (LC 271)(opens leetcode.com)

Two programs have to hand a list of strings to each other, but the channel between them carries a single string and nothing else. Build the class Codec that makes this possible:

  • encode(strs) takes the list of strings strs and returns one string that stands for all of them.
  • decode(s) takes a string made by encode and returns the original list: the same strings in the same order, empty strings included.

The two methods run on different machines. A new Codec decodes what another Codec encoded, so the encoded string has to carry everything, and nothing can be kept in the object between the two calls. The strings may contain any of the 256 ASCII characters, so any separator you might pick can also appear inside a string. The judge runs your code twice, as two separate programs, so nothing stored in a class or a global variable survives either. As in an interview, design the format yourself instead of reaching for library serialization (for example eval or json); the judge itself only checks that the list comes back. It also has machine 1 encode a second, different list right after yours, so a list kept from the last call can't come back.

The judge passes one list, dummy_input, encodes it, decodes the result on a new Codec, and expects the same list back.

Follow-up: can your format work for any set of characters, not only these 256?

Example 1
Input:dummy_input = ["Hello","World"]
Output:["Hello","World"]
encode turns the two strings into one, for example "5#Hello5#World", and a new Codec decodes that string back into ["Hello","World"].
5#Hello05#World1length 5, then Hellolength 5, then World

Encode and Decode Strings

Spot it when: A list of strings must travel as one string and come back exactly, and the strings may hold any character, so no separator can be kept out of them.
Encode and Decode Strings
Python
1class Codec:
2 """Encode and Decode Strings: write each string's length and a "#" in front of it.
3 Time: O(N + L) | Space: O(N + L)
4 """
5 
6 def encode(self, strs: list[str]) -> str:
7 parts: list[str] = [] # One size-prefixed piece per string
8 for s in strs:
9 # 1. Write Each String's Size Before Its Data:
10 # Any character may appear inside a string, so no separator is safe; the reader learns where
11 # a piece ends before it reads a single character of it.
12 parts.append(str(len(s)) + "#" + s)
13 return "".join(parts)
14 
15 def decode(self, s: str) -> list[str]:
16 strs: list[str] = []
17 i = 0 # Always the first digit of a length
18 while i < len(s):
19 # 1. Find Where The Size Ends:
20 # The first "#" from i must end the length, because digits written by encode never contain a "#".
21 j = s.index("#", i)
22 length = int(s[i:j])
23 
24 # 2. Take Exactly That Many Characters And Jump Past Them:
25 # Counting instead of searching means a "#" or a digit inside the text is never read as part
26 # of the format.
27 strs.append(s[j + 1 : j + 1 + length])
28 i = j + 1 + length
29 return strs
Invariant: Every encoded piece is str(len(s)), a "#", then exactly len(s) characters, so in decode the index i always sits on the first digit of a length.

Any character can appear inside a string, so no separator can mark where one ends: write the size before the data instead, str(len(s)) + "#" + s in encode. Digits never contain a "#", so in decode the first "#" from i, j = s.index("#", i), is exactly where the length ends, and the length says where the text ends: take s[j + 1 : j + 1 + length] and jump i = j + 1 + length without reading the text, so the next piece starts on a length again. The trap is reading the text by searching for the next "#" or splitting the message on "#": the strings may hold "#" and digits themselves, as in ["4#ab", "", "#"], and only counting length characters gets past them safely.

See it run

Encode and Decode Strings, step by step

message "" · i -

message "4#4#ab" · i -

message "4#4#ab0#" · i -

message "4#4#ab0#1##" · i -

message "4#4#ab0#1##" · i 0

message "4#4#ab0#1##" · i 0 · length 4

message "4#4#ab0#1##" · i 6 · strs ["4#ab"]

message "4#4#ab0#1##" · i 6 · length 0

message "4#4#ab0#1##" · i 8 · strs ["4#ab", ""]

message "4#4#ab0#1##" · i 8 · length 1

message "4#4#ab0#1##" · i 11 · strs ["4#ab", "", "#"]

message "4#4#ab0#1##" · i 11 · answer ["4#ab","","#"]

The top row is the message. The bottom row is the list of strings: the input of encode first, then the result of decode, filled from column 0. Two quote marks mean an empty string.

  • write a character that encode just wrote into the message
  • string the string in the list strs that is being written or has just been read
  • i the index where the next length starts
  • size a digit of the length that decode reads
  • j the first # after i, which ends the length
  • text a character that decode takes as part of the string
  • #/digit a # or a digit inside the string, which decode takes as text

Start: encode gets strs = ["4#ab", "", "#"]. It writes each string as its length, a "#", then the string.

encode: "4#ab" has length 4, so write "4#4#ab".

encode: "" has length 0, so write "0#".

encode: "#" has length 1, so write "1##".

decode gets only the message "4#4#ab0#1##". It starts with i = 0, on the first digit of a length.

i = 0. The first "#" from i is at j = 1, so length = int("4") = 4.

Take 4 characters after j: s[2:6] = "4#ab". It holds a # or a digit, and they are not read as format because only the count decides where the text ends. Then i = 6.

i = 6. The first "#" from i is at j = 7, so length = int("0") = 0.

Take 0 characters after j: s[8:8] = "". Then i = 8.

i = 8. The first "#" from i is at j = 9, so length = int("1") = 1.

Take 1 character after j: s[10:11] = "#". It holds a # or a digit, and they are not read as format because only the count decides where the text ends. Then i = 11.

Done: i = 11 is the end of the message. decode returns ["4#ab", "", "#"], the same list that encode received.

message
0
1
2
3
4
5
6
7
8
9
10
strs
4#ab
""
#
1 / 12
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Splitting the message on "#"

    Mistake
    def encode(self, strs):
        return "#".join(strs)
    
    def decode(self, s):
        return s.split("#")
    Fix
    strs.append(s[j + 1 : j + 1 + length])
    i = j + 1 + length

    What happens: Any character can appear inside a string, "#" included. A split on "#" cuts such a string into pieces. Reading up to the next "#" cuts it in the same way. decode must take exactly length characters and jump over them with i = j + 1 + length.

    Example: strs = ["4#ab", "", "#"]
    Mistake: returns ['4', 'ab', '', '', '']Fix: returns ['4#ab', '', '#']
  2. 2

    Gotcha 2: Skipping empty strings in encode

    Mistake
    if s:
        parts.append(str(len(s)) + "#" + s)
    Fix
    parts.append(str(len(s)) + "#" + s)

    What happens: An empty string is still one of the strings. It must come back from decode. encode writes it as "0#", a length with no text after it. Skipping it drops it from the result.

    Example: strs = [""]
    Mistake: returns []Fix: returns ['']
  3. 3

    Gotcha 3: Searching with s.index("#") instead of s.index("#", i)

    Mistake
    j = s.index("#")
    Fix
    j = s.index("#", i)

    What happens: Without i, the search starts at the beginning of the message every time. It finds the first "#" again, which ends the first length. For the second string, s[i:j] is then empty, and int("") raises a ValueError.

    Example: strs = ["ab", "cd"]
    Mistake: raises ValueErrorFix: returns ['ab', 'cd']
  4. 4

    Gotcha 4: Reading one digit of the length with int(s[i])

    Mistake
    length = int(s[i])
    Fix
    length = int(s[i:j])

    What happens: A string of 10 or more characters has a length of two or more digits. int(s[i]) reads only the first digit. decode then takes too few characters and loses its place in the message. The next search for "#" finds none and raises a ValueError.

    Example: strs = ["abcdefghij"]
    Mistake: raises ValueErrorFix: returns ['abcdefghij']
Not this one if…
  • A tree's shape must survive, not just a list of strings, as in Construct Binary Tree from Preorder and Inorder Traversal (LC 105): the reader rebuilds which value is whose child from two traversal orders, so no length prefix helps. See Construct Binary Tree
  • The format is given and nests repeat counts in brackets, as in Decode String (LC 394): you parse someone else's encoding, opened and closed by [ and ], instead of designing one. See Nesting Stack

Deep Dive

Why no separator is safe, and why a length is

In Encode and Decode Strings (LC 271), each string can hold any character. A separator character such as "#" can then appear inside a string, and decode cannot tell it from the real separator.

Example: join ["4#ab", "", "#"] with "#" and split the result. The joined text is "4#ab###". The split gives ["4", "ab", "", "", ""], which has five strings and not three.

The fix is to write the size of each string before the string. encode writes str(len(s)) + "#" + s. The reader learns where a piece ends before it reads any character of the piece, so the content of the piece cannot confuse it.

For the same list, the message is "4#4#ab0#1##". The pieces are 4#4#ab, 0# and 1##.

One trace: encode, then decode a string that holds # and digits

The board first runs encode on ["4#ab", "", "#"]. It writes the three pieces into the message. Then decode reads only the message.

In decode, i is on the first digit of a length. j = s.index("#", i) finds the first "#" from i, and int(s[i:j]) is the length. For the first piece, i = 0, j = 1 and length = 4.

Then s[j + 1 : j + 1 + length] takes exactly 4 characters, which are "4#ab". The text holds a digit and a "#", and neither is read as format. This step is marked as the trap on the board. The loop sets i = 6 and does not read the text again.

The second piece has length 0, so it takes no characters and returns "". The third piece has length 1 and takes the single character "#". The loop ends at i = 11, the end of the message.

Why i is always on the first digit of a length

This is the invariant of decode. It holds at the start, because i = 0 and the message starts with a length. It holds after each piece, because i = j + 1 + length is the first character after the text, and encode wrote the next length there.

The first "#" from i ends the length, because the digits that encode writes never contain "#". This is why s.index("#", i) is safe. The "#" inside the text of a string is always after j, and the loop never searches there.

Pass i to s.index. Without i, the search starts at the beginning again, finds the first "#", and s[i:j] is empty for the second string. Then int("") raises a ValueError.

Read all the digits with int(s[i:j]). A string of 10 or more characters has a length with two or more digits. int(s[i]) reads only the first digit and takes too few characters.

Do not skip an empty string in encode. It is written as "0#". Skipping it makes decode return a shorter list.

The cost, and where the same idea is used

Let N be the number of strings and L the total number of characters. encode writes N prefixes and copies L characters, so it is O(N + L). decode reads each prefix once, and each slice copies the characters of one string, so it is also O(N + L). The message holds O(N + L) characters, so the space is O(N + L).

decode never scans the text of a string to find its end. It jumps over the text with i = j + 1 + length. That is why the text can hold any character.

The same idea is used in network formats. The Content-Length header of HTTP/1.1 states the size of the body before the body, so the receiver knows where the message ends.

The separator is a choice. Any character that is not a digit would work in place of "#". The important rule is that the length ends at a character that digits cannot contain.

60-Second Pitch

"The strings can hold any character, so no separator is safe. I write the size before the data: str(len(s)) + "#" + s. In decode, i is always on the first digit of a length. The first "#" from i ends the length, because digits never contain it. Then I take exactly length characters and jump i past them without reading them. So a "#" or a digit inside a string is never read as format. Empty strings work too, as "0#". The time and the space are O(N + L)."

Common Questions

Why not join the strings with a separator?

A string can hold any character, so it can hold the separator too. decode cannot tell a separator inside a string from a real one. For ["4#ab", "", "#"], a split on "#" gives five strings.

Why does the length come before the text?

The reader must know where a piece ends before it reads the piece. With the length first, decode counts characters and never searches the text for an end marker.

Why is the first "#" from i always the end of the length?

The length is written with digits, and a digit is never "#". i is on the first digit of a length, so the first "#" from i is the one that encode wrote after the length.

How are empty strings handled?

encode writes "0#" for an empty string. decode reads the length 0, takes no characters and appends "". Skipping empty strings in encode would lose them.

What are the time and space complexity?

O(N + L) time and O(N + L) space, where N is the number of strings and L is the total number of characters. Each prefix and each character is handled a constant number of times.

SWE Reasoning

Spot it

  • A list of strings must travel as one string and come back exactly.
  • The strings may hold any character, so no separator can be kept out of them.
  • Empty strings are allowed and must come back.
  • The format is read from left to right, and each piece must say how long it is.
  • decode receives only the encoded message, so it cannot use anything else that encode knew.

The invariant, step by step

  1. encode writes str(len(s)) + "#" + s for each string and joins the pieces.
  2. decode starts with i = 0, which is the first digit of the first length.
  3. j = s.index("#", i) is the end of the length, because digits never contain "#".
  4. length = int(s[i:j]) is the size of the text. s[j + 1 : j + 1 + length] is exactly the text.
  5. i = j + 1 + length moves to the next length. The text is never read for format, so a "#" or a digit inside it is safe.
  6. The loop ends when i reaches len(s), and strs holds the original list.

Traps

  • Joining with a separator. ["4#ab", "", "#"] is split into five strings.
  • Searching for the next "#" to find the end of the text. A "#" inside the text ends it too early.
  • Skipping empty strings. An empty string must be written as "0#".
  • s.index("#") without i. The search finds the first "#" again and int("") raises a ValueError.
  • int(s[i]) for the length. A length of 10 or more has several digits.

Practice

Canonical LeetCode:Encode and Decode Strings (LC 271)
Theory Context•Stack

Builds on Digit Manipulation, which you haven’t finished yet.

P2MediumMental model~4 min speedrun

Length-Prefix Encoding

The problem

Encode and Decode Strings (LC 271)(opens leetcode.com)

Two programs have to hand a list of strings to each other, but the channel between them carries a single string and nothing else. Build the class Codec that makes this possible:

  • encode(strs) takes the list of strings strs and returns one string that stands for all of them.
  • decode(s) takes a string made by encode and returns the original list: the same strings in the same order, empty strings included.

The two methods run on different machines. A new Codec decodes what another Codec encoded, so the encoded string has to carry everything, and nothing can be kept in the object between the two calls. The strings may contain any of the 256 ASCII characters, so any separator you might pick can also appear inside a string. The judge runs your code twice, as two separate programs, so nothing stored in a class or a global variable survives either. As in an interview, design the format yourself instead of reaching for library serialization (for example eval or json); the judge itself only checks that the list comes back. It also has machine 1 encode a second, different list right after yours, so a list kept from the last call can't come back.

The judge passes one list, dummy_input, encodes it, decodes the result on a new Codec, and expects the same list back.

Follow-up: can your format work for any set of characters, not only these 256?

Example 1
Input:dummy_input = ["Hello","World"]
Output:["Hello","World"]
encode turns the two strings into one, for example "5#Hello5#World", and a new Codec decodes that string back into ["Hello","World"].
5#Hello05#World1length 5, then Hellolength 5, then World

Encode and Decode Strings

Spot it when: A list of strings must travel as one string and come back exactly, and the strings may hold any character, so no separator can be kept out of them.
Encode and Decode Strings
Python
1class Codec:
2 """Encode and Decode Strings: write each string's length and a "#" in front of it.
3 Time: O(N + L) | Space: O(N + L)
4 """
5 
6 def encode(self, strs: list[str]) -> str:
7 parts: list[str] = [] # One size-prefixed piece per string
8 for s in strs:
9 # 1. Write Each String's Size Before Its Data:
10 # Any character may appear inside a string, so no separator is safe; the reader learns where
11 # a piece ends before it reads a single character of it.
12 parts.append(str(len(s)) + "#" + s)
13 return "".join(parts)
14 
15 def decode(self, s: str) -> list[str]:
16 strs: list[str] = []
17 i = 0 # Always the first digit of a length
18 while i < len(s):
19 # 1. Find Where The Size Ends:
20 # The first "#" from i must end the length, because digits written by encode never contain a "#".
21 j = s.index("#", i)
22 length = int(s[i:j])
23 
24 # 2. Take Exactly That Many Characters And Jump Past Them:
25 # Counting instead of searching means a "#" or a digit inside the text is never read as part
26 # of the format.
27 strs.append(s[j + 1 : j + 1 + length])
28 i = j + 1 + length
29 return strs
Invariant: Every encoded piece is str(len(s)), a "#", then exactly len(s) characters, so in decode the index i always sits on the first digit of a length.

Any character can appear inside a string, so no separator can mark where one ends: write the size before the data instead, str(len(s)) + "#" + s in encode. Digits never contain a "#", so in decode the first "#" from i, j = s.index("#", i), is exactly where the length ends, and the length says where the text ends: take s[j + 1 : j + 1 + length] and jump i = j + 1 + length without reading the text, so the next piece starts on a length again. The trap is reading the text by searching for the next "#" or splitting the message on "#": the strings may hold "#" and digits themselves, as in ["4#ab", "", "#"], and only counting length characters gets past them safely.

See it run

Encode and Decode Strings, step by step

message "" · i -

message "4#4#ab" · i -

message "4#4#ab0#" · i -

message "4#4#ab0#1##" · i -

message "4#4#ab0#1##" · i 0

message "4#4#ab0#1##" · i 0 · length 4

message "4#4#ab0#1##" · i 6 · strs ["4#ab"]

message "4#4#ab0#1##" · i 6 · length 0

message "4#4#ab0#1##" · i 8 · strs ["4#ab", ""]

message "4#4#ab0#1##" · i 8 · length 1

message "4#4#ab0#1##" · i 11 · strs ["4#ab", "", "#"]

message "4#4#ab0#1##" · i 11 · answer ["4#ab","","#"]

The top row is the message. The bottom row is the list of strings: the input of encode first, then the result of decode, filled from column 0. Two quote marks mean an empty string.

  • write a character that encode just wrote into the message
  • string the string in the list strs that is being written or has just been read
  • i the index where the next length starts
  • size a digit of the length that decode reads
  • j the first # after i, which ends the length
  • text a character that decode takes as part of the string
  • #/digit a # or a digit inside the string, which decode takes as text

Start: encode gets strs = ["4#ab", "", "#"]. It writes each string as its length, a "#", then the string.

encode: "4#ab" has length 4, so write "4#4#ab".

encode: "" has length 0, so write "0#".

encode: "#" has length 1, so write "1##".

decode gets only the message "4#4#ab0#1##". It starts with i = 0, on the first digit of a length.

i = 0. The first "#" from i is at j = 1, so length = int("4") = 4.

Take 4 characters after j: s[2:6] = "4#ab". It holds a # or a digit, and they are not read as format because only the count decides where the text ends. Then i = 6.

i = 6. The first "#" from i is at j = 7, so length = int("0") = 0.

Take 0 characters after j: s[8:8] = "". Then i = 8.

i = 8. The first "#" from i is at j = 9, so length = int("1") = 1.

Take 1 character after j: s[10:11] = "#". It holds a # or a digit, and they are not read as format because only the count decides where the text ends. Then i = 11.

Done: i = 11 is the end of the message. decode returns ["4#ab", "", "#"], the same list that encode received.

message
0
1
2
3
4
5
6
7
8
9
10
strs
4#ab
""
#
1 / 12
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Splitting the message on "#"

    Mistake
    def encode(self, strs):
        return "#".join(strs)
    
    def decode(self, s):
        return s.split("#")
    Fix
    strs.append(s[j + 1 : j + 1 + length])
    i = j + 1 + length

    What happens: Any character can appear inside a string, "#" included. A split on "#" cuts such a string into pieces. Reading up to the next "#" cuts it in the same way. decode must take exactly length characters and jump over them with i = j + 1 + length.

    Example: strs = ["4#ab", "", "#"]
    Mistake: returns ['4', 'ab', '', '', '']Fix: returns ['4#ab', '', '#']
  2. 2

    Gotcha 2: Skipping empty strings in encode

    Mistake
    if s:
        parts.append(str(len(s)) + "#" + s)
    Fix
    parts.append(str(len(s)) + "#" + s)

    What happens: An empty string is still one of the strings. It must come back from decode. encode writes it as "0#", a length with no text after it. Skipping it drops it from the result.

    Example: strs = [""]
    Mistake: returns []Fix: returns ['']
  3. 3

    Gotcha 3: Searching with s.index("#") instead of s.index("#", i)

    Mistake
    j = s.index("#")
    Fix
    j = s.index("#", i)

    What happens: Without i, the search starts at the beginning of the message every time. It finds the first "#" again, which ends the first length. For the second string, s[i:j] is then empty, and int("") raises a ValueError.

    Example: strs = ["ab", "cd"]
    Mistake: raises ValueErrorFix: returns ['ab', 'cd']
  4. 4

    Gotcha 4: Reading one digit of the length with int(s[i])

    Mistake
    length = int(s[i])
    Fix
    length = int(s[i:j])

    What happens: A string of 10 or more characters has a length of two or more digits. int(s[i]) reads only the first digit. decode then takes too few characters and loses its place in the message. The next search for "#" finds none and raises a ValueError.

    Example: strs = ["abcdefghij"]
    Mistake: raises ValueErrorFix: returns ['abcdefghij']
Not this one if…
  • A tree's shape must survive, not just a list of strings, as in Construct Binary Tree from Preorder and Inorder Traversal (LC 105): the reader rebuilds which value is whose child from two traversal orders, so no length prefix helps. See Construct Binary Tree
  • The format is given and nests repeat counts in brackets, as in Decode String (LC 394): you parse someone else's encoding, opened and closed by [ and ], instead of designing one. See Nesting Stack

Deep Dive

Why no separator is safe, and why a length is

In Encode and Decode Strings (LC 271), each string can hold any character. A separator character such as "#" can then appear inside a string, and decode cannot tell it from the real separator.

Example: join ["4#ab", "", "#"] with "#" and split the result. The joined text is "4#ab###". The split gives ["4", "ab", "", "", ""], which has five strings and not three.

The fix is to write the size of each string before the string. encode writes str(len(s)) + "#" + s. The reader learns where a piece ends before it reads any character of the piece, so the content of the piece cannot confuse it.

For the same list, the message is "4#4#ab0#1##". The pieces are 4#4#ab, 0# and 1##.

One trace: encode, then decode a string that holds # and digits

The board first runs encode on ["4#ab", "", "#"]. It writes the three pieces into the message. Then decode reads only the message.

In decode, i is on the first digit of a length. j = s.index("#", i) finds the first "#" from i, and int(s[i:j]) is the length. For the first piece, i = 0, j = 1 and length = 4.

Then s[j + 1 : j + 1 + length] takes exactly 4 characters, which are "4#ab". The text holds a digit and a "#", and neither is read as format. This step is marked as the trap on the board. The loop sets i = 6 and does not read the text again.

The second piece has length 0, so it takes no characters and returns "". The third piece has length 1 and takes the single character "#". The loop ends at i = 11, the end of the message.

Why i is always on the first digit of a length

This is the invariant of decode. It holds at the start, because i = 0 and the message starts with a length. It holds after each piece, because i = j + 1 + length is the first character after the text, and encode wrote the next length there.

The first "#" from i ends the length, because the digits that encode writes never contain "#". This is why s.index("#", i) is safe. The "#" inside the text of a string is always after j, and the loop never searches there.

Pass i to s.index. Without i, the search starts at the beginning again, finds the first "#", and s[i:j] is empty for the second string. Then int("") raises a ValueError.

Read all the digits with int(s[i:j]). A string of 10 or more characters has a length with two or more digits. int(s[i]) reads only the first digit and takes too few characters.

Do not skip an empty string in encode. It is written as "0#". Skipping it makes decode return a shorter list.

The cost, and where the same idea is used

Let N be the number of strings and L the total number of characters. encode writes N prefixes and copies L characters, so it is O(N + L). decode reads each prefix once, and each slice copies the characters of one string, so it is also O(N + L). The message holds O(N + L) characters, so the space is O(N + L).

decode never scans the text of a string to find its end. It jumps over the text with i = j + 1 + length. That is why the text can hold any character.

The same idea is used in network formats. The Content-Length header of HTTP/1.1 states the size of the body before the body, so the receiver knows where the message ends.

The separator is a choice. Any character that is not a digit would work in place of "#". The important rule is that the length ends at a character that digits cannot contain.

60-Second Pitch

"The strings can hold any character, so no separator is safe. I write the size before the data: str(len(s)) + "#" + s. In decode, i is always on the first digit of a length. The first "#" from i ends the length, because digits never contain it. Then I take exactly length characters and jump i past them without reading them. So a "#" or a digit inside a string is never read as format. Empty strings work too, as "0#". The time and the space are O(N + L)."

Common Questions

Why not join the strings with a separator?

A string can hold any character, so it can hold the separator too. decode cannot tell a separator inside a string from a real one. For ["4#ab", "", "#"], a split on "#" gives five strings.

Why does the length come before the text?

The reader must know where a piece ends before it reads the piece. With the length first, decode counts characters and never searches the text for an end marker.

Why is the first "#" from i always the end of the length?

The length is written with digits, and a digit is never "#". i is on the first digit of a length, so the first "#" from i is the one that encode wrote after the length.

How are empty strings handled?

encode writes "0#" for an empty string. decode reads the length 0, takes no characters and appends "". Skipping empty strings in encode would lose them.

What are the time and space complexity?

O(N + L) time and O(N + L) space, where N is the number of strings and L is the total number of characters. Each prefix and each character is handled a constant number of times.

SWE Reasoning

Spot it

  • A list of strings must travel as one string and come back exactly.
  • The strings may hold any character, so no separator can be kept out of them.
  • Empty strings are allowed and must come back.
  • The format is read from left to right, and each piece must say how long it is.
  • decode receives only the encoded message, so it cannot use anything else that encode knew.

The invariant, step by step

  1. encode writes str(len(s)) + "#" + s for each string and joins the pieces.
  2. decode starts with i = 0, which is the first digit of the first length.
  3. j = s.index("#", i) is the end of the length, because digits never contain "#".
  4. length = int(s[i:j]) is the size of the text. s[j + 1 : j + 1 + length] is exactly the text.
  5. i = j + 1 + length moves to the next length. The text is never read for format, so a "#" or a digit inside it is safe.
  6. The loop ends when i reaches len(s), and strs holds the original list.

Traps

  • Joining with a separator. ["4#ab", "", "#"] is split into five strings.
  • Searching for the next "#" to find the end of the text. A "#" inside the text ends it too early.
  • Skipping empty strings. An empty string must be written as "0#".
  • s.index("#") without i. The search finds the first "#" again and int("") raises a ValueError.
  • int(s[i]) for the length. A length of 10 or more has several digits.

Practice

Canonical LeetCode:Encode and Decode Strings (LC 271)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•