Problem overview
The task is to turn a slice of strings into one string and back. Encode takes strs and returns a single string. Decode takes that string, and nothing else, and must return the original slice, in the same order. The lesson uses a Codec struct with these two methods, and the tests encode with one Codec value and decode with another, so no state can be shared between the calls.
The strings can hold any printable character, including #, digits and spaces, and a string can be empty. The list itself can also be empty.
For ["lint", "code", "love", "you"] the encoded string is 4#lint4#code4#love3#you, and decoding it gives the same four strings back. For ["3#a", "", "#"] the encoded string is 3#3#a0#1##.
Approach: write each length before its string
The obvious idea is to join the strings with a separator and split on it later. It fails as soon as a string contains the separator. Joining "a#b" and "c" with # gives a#b#c, and strings.Split returns three pieces, ["a" "b" "c"], instead of two. The statement says any printable character can appear, so no separator is safe.
The fix is to stop looking for where a string ends and say it up front. Each string is written as its length, a #, and then the string itself. When decoding, you read the digits up to the first #, turn them into a number, and then take exactly that many characters, whatever they are. A # or a digit inside the string is never examined, because the length already says where the string stops. The next piece starts right after it.
The # after the length is still needed. Without it, a string that starts with a digit would run into its own length: 1 followed by "2x" would look the same as 12 followed by "x". Digits contain no #, so the first # from the current position always marks the end of the length.
Both methods are linear in N, the total length of all the strings. Encoding appends each string once. Decoding moves an index forward through the encoded string and cuts out each piece once, instead of repeatedly shortening the whole string.
Step by step
Encode
- Declare a
strings.Buildernamedencoded. A builder collects text in a growing buffer, so appending a piece doesn't copy everything written so far. Joining with+=on a plain string would copy the whole result each time and make encoding quadratic. - Loop over
strswithrange. The index isn't needed, so it goes to_. - Write each piece:
strconv.Itoa(len(s))turns the length into text, followed by"#"and the string itself. - Return
encoded.String()after the loop.
Decode
- Create the result as an empty slice of strings,
strs := []string{}. - Keep an index
i, starting at 0: the position where the next piece begins. - Loop while
i < len(s). Each round reads one piece. - Find the
#. Startjatiand advance it whiles[j]is not'#'. Indexing a Go string gives a byte, so it is compared with a byte literal in single quotes. - Convert the digits.
s[i:j]is the length as text, andstrconv.Atoireturns the number and an error. The error is skipped with_, because the string came fromEncodeand always holds valid digits. - Cut out the piece: the characters from
j+1up toj+1+length, appended tostrs. - Move
itoj + 1 + length, the start of the next piece. - Return
strs.
The solution in Go
type Codec struct{}
func (this *Codec) Encode(strs []string) string {
var encoded strings.Builder
for _, s := range strs {
encoded.WriteString(strconv.Itoa(len(s)) + "#" + s)
}
return encoded.String()
}
func (this *Codec) Decode(s string) []string {
strs := []string{}
i := 0
for i < len(s) {
j := i
for s[j] != '#' {
j++
}
length, _ := strconv.Atoi(s[i:j])
strs = append(strs, s[j+1:j+1+length])
i = j + 1 + length
}
return strs
}
Go details worth knowing:
- Lengths are in bytes.
len(s)counts bytes and slicing a string cuts by byte offsets, so the two agree with each other. For the lesson's printable ASCII, a byte is a character. For text with other characters it still round-trips, becauseEncodeandDecodeboth count bytes:len("é")is 2. - A
forwith only a condition is Go'swhile. Both loops inDecodeuse that form. s[j]is abyte, not a string. Comparing it with"#"would not compile;'#'is an untyped rune constant that converts to a byte.- Slicing a string is cheap.
s[a:b]shares the original bytes and copies nothing, which keepsDecodelinear. - Empty slice versus nil.
strs := []string{}gives an empty, non-nil slice, so an empty input decodes to[].
Complexity
Time: O(N). Encode writes each string once, and the length prefix adds a few bytes per string. Decode scans each length's digits once and slices each piece once, and i only moves forward.
Space: O(N) for the encoded string and the decoded slice, which are the outputs.
Edge cases and common mistakes
- The empty list and the list with one empty string.
[]encodes to""and decodes to[].[""]encodes to0#and decodes to a slice with one empty string. The length prefix keeps the two apart. - Strings that look like prefixes.
["3#a", "", "#"]encodes to3#3#a0#1##. Decoding reads3, then takes three characters,3#a, and goes on with0#and1##. - Splitting on
#. It breaks on any string that contains the character, as shown above. - Forgetting the separator after the length. Without it, the digits of a length and the digits at the start of a string can't be told apart.
- Cutting
sdown in each round. Usings = s[j+1+length:]would also work in Go, since slicing doesn't copy, but an index is the version that makes the linear cost obvious. - Multi-digit lengths. A string of 12 characters has the prefix
12#. Reading only one digit would break it.
Testing
These inputs and the strings Encode produced for them:
["lint", "code", "love", "you"]encodes to4#lint4#code4#love3#you.["3#a", "", "#"]encodes to3#3#a0#1##.[]encodes to the empty string.[""]encodes to0#.["", ""]encodes to0#0#.["a b", "12#", "x"]encodes to3#a b3#12#1#x.["abcdefghijkl"]encodes to12#abcdefghijkl.
In every case, Decode returns exactly the original slice.
