Problem overview
The class Codec has two methods. encode(strs) takes a list of strings and returns one string that holds all of them. decode(s) takes a string produced by encode and returns the original list, in the same order. The two methods may run in different programs, so decode can use nothing except the string it receives.
The strings are printable ASCII, so they can contain letters, digits, spaces and punctuation such as #, and any of them can be empty. Both methods must run in time proportional to the total length of all the strings.
For ["lint", "code", "love", "you"], encode returns '4#lint4#code4#love3#you' and decode turns that back into the same four strings. For ["3#a", "", "#"] the encoded string is '3#3#a0#1##', and decoding it gives the original three strings, including the empty one.
Approach: write the length first
The obvious idea is to join the strings with a separator and split on it later. That fails as soon as a separator can appear inside a string. For example, joining ["a,b", "c"] with a comma gives "a,b,c", and splitting on commas returns ['a', 'b', 'c']: three strings instead of two. Any single character you choose can also be part of the data, so a separator can't be trusted.
The fix is to stop looking for where a string ends and say it up front. Before each string, write its length, then a #, then the string itself. The decoder reads the digits up to the first #, turns them into a number, and then takes exactly that many characters. It never looks at what those characters are, so a # or a digit inside a string is harmless.
The # after the length is still needed. It marks where the digits end. Without it, "3" followed by the string "1ab" would produce 31ab, and the decoder couldn't tell whether the length is 3, 31 or something else. The first # after a piece's start is always the one encode wrote, because a length is made of digits only.
An empty list encodes to the empty string, which decodes to an empty list. A list holding one empty string encodes to '0#', so the two cases stay different.
Step by step
Encoding
- Create an empty list
parts. It collects one piece per string. Joining the pieces once at the end is linear, while adding each piece onto one growing string can copy that string again each time. - Loop over the strings.
- Build each piece as
str(len(s)) + "#" + s. The length must be converted to text withstrbefore it can be joined to other strings with+. Add the piece toparts. - Return
"".join(parts), which glues the pieces together with nothing between them.
Decoding
- Create an empty result list named
strs. - Keep an index
i, starting at 0, for the position where the next piece begins. - Loop while
i < len(s). Each round reads one whole piece: a length, a#, then that many characters. - Find the
#. Startjatiand move it forward whiles[j]isn't"#". - Read the length. The slice
s[i:j]holds the digits, andintturns them into a number. - Take the string: the
lengthcharacters starting right after the#, which is the slices[j + 1:j + 1 + length]. Add it to the result. - Move
itoj + 1 + length, the start of the next piece. - Return the result. An empty
snever enters the loop and gives an empty list.
The solution in Python
class Codec:
def encode(self, strs: List[str]) -> str:
parts = []
for s in strs:
parts.append(str(len(s)) + "#" + s)
return "".join(parts)
def decode(self, s: str) -> List[str]:
strs = []
i = 0
while i < len(s):
j = i
while s[j] != "#":
j += 1
length = int(s[i:j])
strs.append(s[j + 1:j + 1 + length])
i = j + 1 + length
return strs
A few Python details matter here. Strings are immutable, so a + b always builds a new string. That is why encode collects pieces in a list and calls "".join once. A slice such as s[j + 1:j + 1 + length] includes the start index and excludes the end index, so it holds exactly length characters. When length is 0 the slice is empty, which is how an empty string comes back. Slicing also never raises an error for bounds that run past the end, but with strings made by encode that never happens.
Complexity
encode visits each string once. Computing len(s) is constant time, and building a piece and joining the pieces copy each character a constant number of times, so the time is O(N), where N is the total length of all the strings. (The pieces also carry the length digits and the #, which add a little per string and don't change the order.) The extra space is O(N) for the pieces and the output.
decode moves the pointers j and i only forward. The inner loop scans the digits of one length, and each slice copies one string. Every character of s is passed over a constant number of times, so the time is O(N). The result list takes O(N) space.
Moving an index through s is what keeps this linear. Repeatedly cutting the front off the string, with something like s = s[j + 1 + length:], copies the rest of the string every time, and that adds up to quadratic time on long inputs.
Edge cases and common mistakes
- An empty list versus a list with one empty string.
[]encodes to''and[""]encodes to'0#'. Decoding gives back[]and['']. - Strings that look like pieces.
["12#ab", "5"]encodes to'5#12#ab1#5'. The decoder reads 5, takes12#abas one string, then reads 1 and takes5. Because it counts characters, the content is never parsed. - Lengths with several digits. A string of 150 characters has the prefix
150#. This is why the decoder scans up to the#instead of reading a single digit. - Forgetting
str().len(s) + "#"raises aTypeError, because an integer can't be added to a string. - Off-by-one in the slice. The string starts at
j + 1, just after the#, not atj.
Testing
Try these lists, encoding each and decoding the result. The decoded list should equal the input every time.
| strs | encode(strs) |
|---|---|
["lint", "code", "love", "you"] | '4#lint4#code4#love3#you' |
["3#a", "", "#"] | '3#3#a0#1##' |
[] | '' |
[""] | '0#' |
["", "", ""] | '0#0#0#' |
["12#ab", "5"] | '5#12#ab1#5' |
