Problem overview
The class Codec has two methods. encode(strs: List[String]) packs a list of strings into one String. decode(s: String) takes that string, made by a different Codec object, and returns the original List[String], in the same order. Strings can hold any printable character, including #, digits and spaces, and they can be empty.
For ["lint","code","love","you"], encode returns 4#lint4#code4#love3#you, and decode turns it back into the same four strings. For ["3#a","","#"], the encoded string is 3#3#a0#1##, and it still decodes to the three original strings. An empty list encodes to "", while a list holding one empty string, [""], encodes to 0#, so the two stay different.
Approach: say how long each string is
The obvious idea is to join the strings with a separator, such as #. It breaks as soon as a string contains the separator. Joining "a#b" and "c" gives a#b#c, and splitting on # returns three pieces instead of two. Any single separator character can appear inside a string, so no choice of character is safe.
The fix is to stop searching for a boundary and write down the length instead. In front of each string, put its length and a #: <length>#<string>. To decode, read the digits up to the next #, then take exactly that many characters, whatever they are, and the next piece starts right after them. The decoder never looks inside a string, so a # or a digit in the data can't confuse it.
The # after the length is still needed. Without it, the strings "1a" and "b" would encode as 21a1b, and the decoder couldn't tell where the digits of one length end and the string begins. The # is only read while scanning the digits, and a length is made of digits only, so the first # after the start of a piece is always the one encode wrote.
Both methods should run in O(N) time, where N is the total length of all the strings. That shapes two choices: build the output with a StringBuilder instead of repeated +, and walk through the encoded string with an index instead of cutting it into shorter copies.
Step by step
encode
- Create a
StringBuildernamedencoded. It appends to a growing buffer, so adding a piece doesn't copy everything written so far. - Loop over the strings with
for (s <- strs). - Append the three parts of each piece: the length, the
'#'and the string itself. Eachappendreturns the builder, so the calls chain on one line. - Return
encoded.toString. It is the last expression of the method, so it is the result.
decode
- Create an
ArrayBuffer[String]namedstrsto collect the strings as they are read. - Keep an index
iin avar, starting at 0. It marks where the next piece begins. - Loop while
i < s.length. Each round reads one piece. An emptysnever enters the loop, which is why an empty list comes back as an empty list. - Find the
#: startjatiand move it forward whiles(j)is not'#'. - Read the length:
s.substring(i, j)holds the digits, andtoIntturns them into a number. - Take the string: exactly
lengthcharacters starting atj + 1, added tostrswith+=. - Move
itoj + 1 + length, where the next piece starts. - Return
strs.toList, the buffer converted to theListthe method promises.
The solution in Scala
class Codec {
def encode(strs: List[String]): String = {
val encoded = new StringBuilder()
for (s <- strs) {
encoded.append(s.length).append('#').append(s)
}
encoded.toString
}
def decode(s: String): List[String] = {
val strs = scala.collection.mutable.ArrayBuffer[String]()
var i = 0
while (i < s.length) {
var j = i
while (s(j) != '#') {
j += 1
}
val length = s.substring(i, j).toInt
strs += s.substring(j + 1, j + 1 + length)
i = j + 1 + length
}
strs.toList
}
}
The Scala details worth noticing:
StringBuilder.appendaccepts many types.append(s.length)takes anInt,append('#')aCharandappend(s)aString. Each call writes the text form of its argument, soappend(12)adds the characters1and2.- Single quotes are a
Char, double quotes aString.s(j) != '#'compares a character with a character. Scala reads a character with parentheses,s(j), not square brackets. substring(start, end)stops beforeend.s.substring(i, j)is the digits fromiup to, but not including, the#atj. The string itself iss.substring(j + 1, j + 1 + length), and whenlengthis 0 that is an empty string.toIntparses a decimal string. It turns"12"into 12. It throwsNumberFormatExceptionon anything else, which can't happen here because the digits come fromencode.valversusvar.encoded,strsandlengthnever get reassigned, so they areval. The buffers still change, becausevalfixes the reference, not the contents.iandjmove, so they arevar.- Mutable buffer, immutable result.
ArrayBuffergrows with+=;toListcopies it into an immutableList, which is whatdecodereturns.
Complexity
Time: O(N) for both methods, where N is the total length of the strings. encode appends each character once, plus a few digits for each length. decode moves j over the digits of each length and then jumps i past the string in one step. Every character of s is passed over once, and each substring copies only the characters it returns.
Space: O(N) for the encoded string and the decoded list, which are the outputs. Beyond them, only a few integers are kept.
Edge cases and common mistakes
- Empty list and list with an empty string.
List()encodes to""and decodes toList().List("")encodes to0#and decodes to a list holding one empty string. The length prefix keeps them apart. - A string made of separators or digits.
List("12#", "a b", "0")encodes to3#12#3#a b1#0and decodes correctly, because the decoder only trusts the length. - Several digits in a length. A string of 200 characters has the length prefix
200#. Reading only one digit would break such strings, which is why the decoder scans up to the#. - Forgetting the
#. Prefixing the length alone makes"1a"followed by"b"ambiguous. - Using
split. Splitting on#fails on any string that contains one. - Building with
+in a loop.result = result + piececopies the whole result each time, which makesencodeslower than linear with 100,000 strings. - Cutting
sshorter while decoding. Takings = s.substring(...)after every piece copies the rest of the string each time. Moving an index avoids it.
Testing
List("lint", "code", "love", "you")encodes to4#lint4#code4#love3#youand decodes back to the same list.List("3#a", "", "#")encodes to3#3#a0#1##and decodes back to the same list.List()encodes to""and decodes toList().List("")encodes to0#and decodes toList("").List("", "")encodes to0#0#and decodes toList("", "").- A list of 100,000 copies of
"abcdefghij"decodes back to an equal list.
