Problem overview
The class Codec has two methods. Encode takes a List<string> and returns one string that holds all of them. Decode takes that string, built by a different Codec object, and returns the original list in the same order. Decode can use only the string it receives, with no shared state.
The strings are printable ASCII and may contain #, digits and spaces. A string can also be empty, and the list can be empty.
For ["lint", "code", "love", "you"], one valid encoding is 4#lint4#code4#love3#you. For ["3#a", "", "#"] the encoding is 3#3#a0#1##, and decoding it returns exactly the same three strings. An empty list encodes to the empty string, while [""] encodes to 0#, so the two are never confused.
Approach: write the length first
The obvious plan is to join the strings with a separator, such as a comma or a #, and split on it later. That fails here because any printable character can appear inside a string, so the separator may also be part of the data. Escaping the separator works but makes the code fiddly.
A cleaner idea is to say how long each string is before writing it. Each string becomes <length>#<string>. When decoding, read the digits up to the next #, convert them to a number, and take exactly that many characters. Those characters are never inspected, so a # or a digit inside them can't confuse the decoder. The next piece starts right after them.
The # after the length is still needed. Without it, "12" followed by a string starting with 3 would be written as 123..., and the decoder couldn't tell where the length ends. The decoder only looks for a # while it is reading a length, and a length consists of digits only, so the first # it meets is the right one.
Both methods must run in O(N), where N is the total length of all the strings. That has two consequences. Encode shouldn't add to a string in a loop, because each addition copies everything built so far. Decode shouldn't cut the remaining input into shorter and shorter copies. It moves an index through the one string instead.
Step by step
Encode
- Create a
StringBuildernamedencoded. It collects text piece by piece without copying what is already there. - Loop with
foreachover the strings instrs. - Append three things for each string
s: its lengths.Length, the character'#', andsitself.Appendreturns the builder, so the three calls can be chained on one line. - Return
encoded.ToString()after the loop.
Decode
- Create the result list,
strs, empty for now. - Declare
ias 0. It is the position where the next piece starts. - Loop while
i < s.Length. Each round reads one whole piece. An empty input skips the loop and returns an empty list. - Start
jatito look for the#after the length's digits. - Move
jforward with an inner loop that runs whiles[j]isn't'#'. - Parse the length. The digits sit between
iandj. C#'sSubstringtakes a start and a length, so the call iss.Substring(i, j - i), andint.Parseturns it into anint. - Add the string to
strs:s.Substring(j + 1, length), thelengthcharacters right after the#. - Move
itoj + 1 + length, the start of the next piece. - Return
strsafter the loop.
The solution in C#
class Codec {
public string Encode(List<string> strs) {
StringBuilder encoded = new StringBuilder();
foreach (string s in strs) {
encoded.Append(s.Length).Append('#').Append(s);
}
return encoded.ToString();
}
public List<string> Decode(string s) {
List<string> strs = new List<string>();
int i = 0;
while (i < s.Length) {
int j = i;
while (s[j] != '#') {
j++;
}
int length = int.Parse(s.Substring(i, j - i));
strs.Add(s.Substring(j + 1, length));
i = j + 1 + length;
}
return strs;
}
}
The C# details worth knowing:
- Method names are PascalCase. C# methods are conventionally
EncodeandDecode, notencodeanddecode. Names are case-sensitive, so a lowercaseencodeis a different, undefined method. Append(int)writes the digits.StringBuilder.Appendhas overloads forintandchar, soAppend(s.Length)adds the decimal text of the number, andAppend('#')adds one character. Using'#'rather than"#"avoids creating a string for one character.Substring(start, length). The second argument is a count, not an end position. This differs fromslicein other languages and is the usual source of off-by-one errors here.- Strings are immutable. Every
+=on astringbuilds a new one, so appending in a loop costs more than linear time in total.StringBuilderkeeps the work linear. - The helper types come from namespaces.
StringBuilderlives inSystem.TextandList<T>inSystem.Collections.Generic. In a file of your own, add the matchingusinglines.
Complexity
Time: O(N). Encode appends each string once, and each append costs time proportional to the characters written. Decode moves j over each length's digits and copies each string once with Substring. The index i only moves forward, so no character is scanned more than a constant number of times. The digits of the lengths add at most a few characters per string.
Space: O(N) for the output of each method. Apart from the output, there is no extra storage beyond a few variables.
Edge cases and common mistakes
- Empty list.
Encodereturns""andDecode("")returns an empty list. - List with one empty string. It encodes to
0#and decodes to[""]. This is why the length is written even when it is 0. - Strings that look like the format.
"3#a"is read back correctly because the decoder takes exactly 3 characters after the first#and never looks inside them. - Using a bare separator. Joining with
#breaks as soon as a string contains a#. - Searching for the
#from the wrong place. The search must begin ati, the start of the current length. Searching from the start ofswould find an earlier#. - Passing the wrong second argument to
Substring.s.Substring(i, j)is wrong unlessiis 0; the length of the digits isj - i. - Cutting the input down. Replacing
swiths.Substring(j + 1 + length)every round copies the rest of the text each time, which makesDecodequadratic.
Testing
Each input below was encoded and the result decoded again:
["lint", "code", "love", "you"]encodes to4#lint4#code4#love3#youand decodes to the same list.["3#a", "", "#"]encodes to3#3#a0#1##and decodes to the same list.[]encodes to the empty string and decodes to[].[""]encodes to0#and decodes to[""].["a", "b"]encodes to1#a1#band decodes to["a", "b"].
