TL;DR
Length-prefix framing ("5#hello") β O(total characters) time for both directions, O(1) extra space beyond the output.
Approach 1 β Brute force
This is a pure design problem β there is no algorithm to speed up β so the ladder starts at the naive design and fixes its correctness.
The naive design: glue the strings together with a separator character and split on it.
from typing import List
class Codec:
def encode(self, strs: List[str]) -> str:
return ",".join(strs)
def decode(self, s: str) -> List[str]:
return s.split(",")
Complexity: O(total characters) time β but itβs wrong, and no constraint tuning fixes that:
["a,b"] and ["a", "b"] both encode to "a,b" β the payload can contain the separator.
[] and [""] both encode to "".
Any fixed separator fails the same way, because the problem guarantees strings may contain any character.
Approach 2 β Escaping
The insight: if the separator can appear in data, make occurrences in data look different: pick an escape character, double it whenever it appears literally, and let escape-plus-marker mean βend of item.β Terminating every item (rather than separating items) also distinguishes [] from [""]. This is the classic escaping scheme used by string literals and CSV quoting.
from typing import List
class Codec:
def encode(self, strs: List[str]) -> str:
# "/" is the escape char: "//" = literal "/", "/;" = end of item.
return "".join(s.replace("/", "//") + "/;" for s in strs)
def decode(self, s: str) -> List[str]:
res: List[str] = []
cur: List[str] = []
i = 0
while i < len(s):
if s[i] == "/":
if s[i + 1] == "/":
cur.append("/") # escaped literal slash
else:
res.append("".join(cur)) # "/;" closes the item
cur = []
i += 2
else:
cur.append(s[i])
i += 1
return res
Walkthrough on ["a#b", "#", ""] (the questionβs third example):
- Encode: no slashes to double, so items become
"a#b/;", "#/;", "/;" β "a#b/;#/;/;".
- Decode scans left to right:
a, #, b are plain; /; closes item β "a#b".
# plain; /; closes β "#"; final /; closes an empty item β "". Result: ["a#b", "#", ""].
Complexity: O(total characters) time and output space β but the decoder must inspect every byte of payload, and the encoding can double in size (all-slash input).
Approach 3 β Length prefix
The insight: searching for a separator is the whole problem β so stop searching. Announce each stringβs length up front, then the decoder jumps over the payload without reading it; payload bytes can be anything, including digits and #. This is length-prefix framing, the same idea network protocols (e.g. HTTP chunked encoding) use to delimit binary-safe messages.
from typing import List
class Codec:
def encode(self, strs: List[str]) -> str:
return "".join(f"{len(s)}#{s}" for s in strs)
def decode(self, s: str) -> List[str]:
res: List[str] = []
i = 0
while i < len(s):
j = s.find("#", i) # first "#" after the digits
length = int(s[i:j])
start = j + 1
res.append(s[start : start + length])
i = start + length
return res
Walkthrough on ["hello", "world"] (the questionβs first example):
- Encode:
"5#hello" + "5#world" β "5#hello5#world".
- Decode at
i=0: # found at 1, length = 5, payload s[2:7] = "hello", jump to i=7.
- At
i=7: # at 8, length = 5, payload "world", i=14 = end. Result: ["hello", "world"].
Why a # inside a payload never confuses it: the decoder only runs find at positions it knows are headers β e.g. ["2#ab"] encodes to "4#2#ab", and after reading length = 4 the entire "2#ab" is consumed blindly.
Complexity: O(total characters) for both encode and decode; output-sized space. The header adds only O(log L) characters per string, and no payload byte is ever inspected β strictly better constants than escaping.
Common pitfalls
- Any bare-delimiter scheme, however exotic the character β the problemβs whole point is that payloads are unrestricted.
- Conflating
[] with [""]: separator-between joins encode both as ""; terminate every item (Approach 2) or note that length-prefix naturally distinguishes "" vs "0#".
- In the length-prefix decoder, assuming the length is one digit β
"12#hello, world" needs find, not s[i] alone.
- Off-by-one on the jump: the next header starts at
start + length, not start + length + 1 β there is no separator after the payload.
Pattern takeaway
When data must embed arbitrary data, in-band separators are a losing game; the two industrial-strength answers are escaping (make the separator unmistakable) and length-prefix framing (make searching unnecessary). Prefer the length prefix: self-describing, binary-safe, and linear with no rescanning β and it generalizes from interview problems to real wire protocols.