Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Don't join with a delimiter — any delimiter can appear inside the data. Use length-prefix framing: encode each string as `len(s) + '#' + s`. To decode, read digits up to the next `#` to get the length, then take exactly that many characters as the string. Because the length tells you precisely how far to jump, the `#` inside the data is never mistaken for a separator.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you recognise that any single-character (or even multi-character) delimiter is unsafe because the data can contain it, and reach for length-prefix framing instead. It's a serialization-correctness problem disguised as string manipulation.
Where candidates slip
Joining with a 'rare' delimiter like a comma or a non-printable char and assuming the input won't contain it — it can, and decode silently splits in the wrong place.
How to stand out
Name the length-prefix pattern, show it handles empty strings and embedded delimiters, decode by parsing the length then slicing exactly that many chars, and connect it to real protocols (HTTP Content-Length, netstrings).
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewSerialize a list of arbitrary stringsCan data contain your delimiter?No / guaranteed safeYes (256 ASCII)Explore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Single-Character Delimiter Join (Naive — breaks on the data)
1Hint 1Explore hintHide hint
The simplest idea: join the strings with some separator character and split on it during decode.
2Hint 2Explore hintHide hint
Pick any separator — comma, space, even a 'rare' non-printable char. Now ask: what if a string in the list actually contains that character?
3Hint 3Explore hintHide hint
If the data can contain your separator, decode splits in the wrong place. With all 256 ASCII characters allowed, NO single character is safe.
A · Core idea
The instinctive first attempt is to glue the strings together with a separator, e.g. ",".join(strs), and split(",") to decode.
Why it feels right. It's a one-liner, it's fast, and it works on every test case you'd naively write — ["neet","code"] round-trips cleanly through "neet,code".
Why it's fundamentally broken. The decode step assumes the separator never appears inside a string. But the data may contain any of the 256 ASCII characters — including your separator. Encode ["a,b", "c"] with a comma and you get "a,b,c"; decode splits into ["a", "b", "c"] — three strings, not two. People reach for an 'exotic' delimiter like \x00 or "#:#" to dodge this, but that only narrows the failure, it doesn't remove it: the prompt explicitly allows every character, so any chosen delimiter can occur in the payload.
Why escaping is the wrong fix. You can escape the delimiter (e.g. replace # with \# and \ with \\), but now you've signed up for careful, bidirectional escape/unescape logic that's easy to get subtly wrong. Length-prefix framing sidesteps escaping entirely: by recording how long each string is, decode never has to interpret the payload at all.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
strs = ["a,b", "c"], delimiter = ','encode
join with ',' → "a,b" + "," + "c"
encoded = "a,b,c"["a", "b", "c"] — WRONG (should be ["a,b", "c"])C · Code with teaching comments
Reference implementation previewpublic class Codec { public String encode(List<String> strs) { return String.join(",", strs); // unsafe: ',' may appear in the data } Explore the full codeHide the full code
D · Why it works and complexityencode O(total chars), decode O(total chars) timeO(total chars) spaceOpen the invariant and proofExplore the proofHide the proof
A delimiter scheme is only correct if the delimiter can never appear in the data — an assumption you almost never actually have. The robust alternative is to stop relying on a 'special' character and instead store the boundary information (the length) out-of-band in a header.
Complexityencode O(total chars), decode O(total chars) time, O(total chars) space — Joining and splitting both touch every character once → O(total chars) time and space. The cost isn't the problem — correctness is.
E · Edge cases and pitfalls5strs = ["", ""] — Join gives "," and split also struggles with trailing/empty fields — empty strings are another failure mode of naive delimiters.Explore edge casesHide edge cases
Edge cases handled
strs = ["", ""]Join gives "," and split also struggles with trailing/empty fields — empty strings are another failure mode of naive delimiters.strs = ["a#b"] with '#' delimiterDecodes to ["a", "b"] — proof that even a 'rare' delimiter fails when it appears in the payload.
Pitfalls
- Believing a 'rare' or non-printable delimiter is safe — the spec allows all 256 ASCII chars, so none is.
- Trailing-delimiter ambiguity: "a," could mean ["a", ""] or ["a"] depending on split semantics.
- Reaching for escaping to patch it — correct but fiddly; length-prefix avoids the whole problem.
Approach 2 of 2 · Optimal
Length-Prefix (Chunked) Encoding — len#str (Optimal)
1Hint 1Explore hintHide hint
The problem with delimiters is not knowing where a string ends. What if you wrote down the length first?
2Hint 2Explore hintHide hint
Encode each string as length + a marker + the string itself, e.g. "4#neet". The marker only separates the length digits from the payload.
3Hint 3Explore hintHide hint
Decode: read digits up to the first '#' to get the length, then take exactly that many characters as the string — the embedded characters (even '#') are read as data, never as boundaries.
A · Core idea
Frame each string with its length: write len(s), a separator #, then s itself. The encoded form of ["neet","code"] is "4#neet4#code".
Why the length makes the payload safe. The breakthrough is that decode never has to interpret the string content. It reads the digits before the first # to learn the length L, then blindly consumes the next L characters as the string — whatever they are. If one of those L characters is itself a #, it doesn't matter, because we're counting characters, not scanning for a separator. The # is special only in the length header, and there it can't be confused with a digit. This is what the comma-join could never do.
Why we need the `#` at all. The length is a variable number of digits (5, 42, 100...), so we need to know where the digits stop and the payload begins. The # is that boundary. We can't be confused by a # in the data because by construction the parser is reading digits until the first #, and digits can't be #.
Why the cursor math must be exact. After reading a chunk that started at index i with its # at index j, the payload is s[j+1 : j+1+L] and the next chunk starts at j + 1 + L. An off-by-one here — say starting the next chunk at j + L — shifts every subsequent string and corrupts the whole decode. Walk this pointer arithmetic carefully.
Why empty strings just work. "" has length 0, so it encodes as "0#" — a header with no payload. Decode reads 0, consumes zero characters, and correctly produces "". No special case needed, which is a sign the design is right.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
decode("4#neet4#code")Array state
read length 4
digits before '#' → 4; payload = next 4 chars
["neet", "code"]C · Code with teaching comments
D · Why it works and complexityencode O(total chars), decode O(total chars) timeO(total chars) spaceOpen the invariant and proofExplore the proofHide the proof
Storing the length turns an ambiguous 'where does this end?' scan into an unambiguous 'jump exactly this far' move. That's the essence of length-prefix framing — the same trick HTTP uses with Content-Length and that netstrings and TCP use to delimit messages. Whenever a delimiter could collide with the data, prefix the length instead.
Complexityencode O(total chars), decode O(total chars) time, O(total chars) space — Encode appends a length header plus each string once → O(total chars). Decode scans left to right, reading each length then jumping exactly that many characters, touching every character a constant number of times → O(total chars). Extra space is the encoded buffer / output list, O(total chars).
E · Edge cases and pitfalls5strs = ["#3", ""] — Encodes to "2##30#". Decode reads length 2 → payload "#3" (the inner '#' is data), then length 0 → "". Proves embedded separators and empty strings both work.Explore edge casesHide edge cases
Edge cases handled
strs = ["#3", ""]Encodes to "2##30#". Decode reads length 2 → payload "#3" (the inner '#' is data), then length 0 → "". Proves embedded separators and empty strings both work.strs = [""]Encodes to "0#"; decode reads length 0, slices zero chars → [""]. Empty string handled with no special case.
Pitfalls
- Off-by-one on the cursor (i = j + length instead of j + 1 + length) — shifts and corrupts all following chunks.
- Searching for the length's end with anything other than 'first # after the cursor' — a later '#' in the payload must not be used.
- Assuming the separator must be absent from the data — it can be present; only the header '#' is interpreted.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic class Codec { public String encode(List<String> strs) { StringBuilder sb = new StringBuilder(); for (String s : strs) { sb.append(s.length()).append('#').append(s);Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Trusting a 'safe' delimiter
No single character is safe when the data may contain all 256 ASCII characters. Even an exotic separator can appear in the payload and break decode.
Explore the code comparisonHide the code comparison
Trusting a 'safe' delimiter
No single character is safe when the data may contain all 256 ASCII characters. Even an exotic separator can appear in the payload and break decode.
Don't write this
def encode(self, strs):
return "\x00".join(strs) # \x00 can still appear in the data
def decode(self, s):
return s.split("\x00")Write this instead
def encode(self, strs):
return "".join(f"{len(s)}#{s}" for s in strs) # length, not a delimiter, marks boundaries2Off-by-one when advancing the cursor
After reading the '#' at index j and a payload of length L, the next chunk starts at j+1+L. Using j+L (or j+1) leaves a stray character or eats one, corrupting every following string.
Explore the code comparisonHide the code comparison
Off-by-one when advancing the cursor
After reading the '#' at index j and a payload of length L, the next chunk starts at j+1+L. Using j+L (or j+1) leaves a stray character or eats one, corrupting every following string.
Don't write this
String word = s.substring(j + 1, j + 1 + length);
result.add(word);
i = j + length; // WRONG — drops/duplicates a characterWrite this instead
String word = s.substring(j + 1, j + 1 + length);
result.add(word);
i = j + 1 + length; // start exactly at the next header3Scanning for the length's end incorrectly
The length ends at the FIRST '#' after the cursor. If you scan for some other occurrence (or split on all '#'), a '#' inside the payload gets mistaken for the header boundary.
Explore the code comparisonHide the code comparison
Scanning for the length's end incorrectly
The length ends at the FIRST '#' after the cursor. If you scan for some other occurrence (or split on all '#'), a '#' inside the payload gets mistaken for the header boundary.
Don't write this
j = s.rfind('#') # WRONG — finds the last '#', not this chunk's header
length = int(s[i:j])Write this instead
j = s.index('#', i) # first '#' at/after the cursor is this chunk's header
length = int(s[i:j])Revise
Carry the pattern into the interview
Pattern to remember
Length-prefixed (chunked) encoding — `len#payload`
Code pattern to memorize
encode: ''.join(str(len(s)) + '#' + s for s in strs)
decode:
i = 0
while i < len(s):
j = s.index('#', i)
length = int(s[i:j])
word = s[j+1 : j+1+length]
i = j + 1 + lengthRules to remember
- 1Prefix each string with its length and a separator (
len#) — the length, not the separator, decides where the string ends. - 2On decode, read digits until the first `#` to get the length, then consume exactly that many characters.
- 3The separator
#is only meaningful in the length header; inside the payload it's just data. - 4Handle the empty string naturally:
0#encodes "" with no payload bytes. - 5Advance the cursor to `j + 1 + length` after each chunk — off-by-one here corrupts every following string.
Use this pattern when
- Serialize a list of arbitrary strings into one string and recover it exactly
- Data may contain ANY character, including your would-be delimiter
- Designing a wire/file format where chunk boundaries must be unambiguous
- Any 'pack many fields into one buffer, then split losslessly' task
Choose another pattern when
- Strings are guaranteed delimiter-free → a simple split on that char is fine
- Fixed-width fields → pad to a known width instead of length-prefixing
- Only one string → no framing needed at all
- You control both ends with a typed format (JSON/protobuf) → use that library
Interview takeaway
Length-prefix framing is the bulletproof way to serialize variable-length data: by storing how long each chunk is, you never have to guess where it ends, so no character is ever 'special' in the payload. It's the same idea behind HTTP Content-Length, netstrings, and TCP framing — once you see it, every 'delimiter breaks on the data' bug disappears.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical length-prefix (chunked) framing. Recognise it whenever you must serialize variable-length data that could contain any character: store each chunk's length so boundaries are unambiguous. Same idea as HTTP Content-Length, netstrings, and binary-tree serialization with markers.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Serialize and Deserialize Binary TreeComing soonSame serialization mindset — pick an unambiguous framing (preorder + null markers) so decode is lossless.
- Encode and Decode TinyURLComing soonEncoding for retrieval rather than framing — map long URL to a short key and back via a hash map.
- String CompressionComing soonAlso packs counts next to characters (run-length); related length-aware encoding idea.
- Count and SayComing soonBuilds strings as count+character runs — another length-prefix-flavoured construction.