Problem overview
The class Codec has two methods. encode takes a vector<string> and returns one string that holds all of them. decode takes that string, and nothing else, and rebuilds the original vector: the same strings in the same order. A test encodes with one Codec object and decodes with another, so no information can travel except the encoded string itself.
The strings can hold any printable character, including #, digits and spaces, and a string can be empty. The list itself can also be empty. Take ["3#a", "", "#"]: it has to come back as exactly three strings, the first one containing a digit and a hash, the second one empty, the third one a lone hash.
Both methods must run in O(N) time, where N is the total length of all the strings.
Approach: say how long each string is
The obvious idea is to join the strings with a separator and split on it when decoding. It fails because the separator can appear inside a string. Joining "a#b" and "c" with # after each string gives a#b#c#, and splitting that on # produces three strings, not two. Any single character you pick can be part of the data, so a separator alone can never be reliable.
The fix is to stop searching for where a string ends and write it down instead. In front of each string, put its length, then a #, then the string. Decoding reads the digits up to the first #, converts them to a number, and then takes exactly that many characters without looking at what they are. A # or a digit inside the string is just a character that is part of the count. The next piece starts right after those characters.
The # after the length is still needed, because lengths have different numbers of digits. Without it, a string that starts with a digit would run into its own length, and 1 followed by "2" could not be told apart from 12. The reader only ever looks for a # while it is reading a length, and a length is made of digits only, so the first # it meets is always the one that encode wrote.
For ["lint", "code", "love", "you"] the encoded string is 4#lint4#code4#love3#you. For ["3#a", "", "#"] it is 3#3#a0#1##. An empty list encodes to the empty string, and [""] encodes to 0#, so those two inputs stay different.
Step by step
For encode:
- Create an empty string named
encoded. It collects the pieces. - Loop over
strswith a range-basedfor, taking each element asconst string& s. The reference avoids copying every string just to look at it. - Append the piece with
+=: the length as text, a#, then the string.to_stringturns the number into digits. - Return
encodedafter the loop.
For decode:
- Create the result vector
strs, empty. - Keep an index
istarting at 0. It always marks where the next piece begins. - Loop while
i < s.size(). Each round reads one complete piece, so an emptysnever enters the loop. - Find the
#. Startjatiand move it forward whiles[j]is not'#'. The digits of the length are the characters fromiup to, but not including,j. - Convert the digits.
s.substr(i, j - i)copies them, becausesubstrtakes a start position and a length, andstoiturns that text into theintnamedlength. - Take the string. It starts right after the
#, atj + 1, and is exactlylengthcharacters long. Push it intostrs. - Move
itoj + 1 + length, the start of the next piece. Moving an index forward is what keeps the work linear: cuttingsshorter after every piece would copy the rest of it again and again. - Return
strs.
The solution in C++
class Codec {
public:
string encode(vector<string>& strs) {
string encoded;
for (const string& s : strs) {
encoded += to_string(s.size()) + "#" + s;
}
return encoded;
}
vector<string> decode(string s) {
vector<string> strs;
int i = 0;
while (i < s.size()) {
int j = i;
while (s[j] != '#') {
j++;
}
int length = stoi(s.substr(i, j - i));
strs.push_back(s.substr(j + 1, length));
i = j + 1 + length;
}
return strs;
}
};
C++ details worth a closer look:
to_string(s.size()).size()returns an unsigned type, andto_stringhas an overload for it, so the length is written as plain digits.+=grows in place.encoded += xappends to the existing string and reuses its spare capacity, so building the result piece by piece is linear. Writingencoded = encoded + xwould build a new string each time and copy everything collected so far.const string&. A reference to a constant string reads each element without copying it. Plainstring sin the loop would copy every string.substr(pos, count). The second argument is a length, not an end position. That is why the digits ares.substr(i, j - i), and why the payload iss.substr(j + 1, length).i < s.size()with anint. Comparing a signedintwith an unsignedsize()compiles, and some compilers warn about it. It is safe here becauseiis never negative.- Parameters.
encodetakesstrsby reference, because a vector passed by value is copied.decodetakessby value, which costs one copy of the whole encoded string. That copy is O(N), so the total stays linear. - The inner loop trusts the input. It stops only at a
#, so it relies onshaving been produced byencode. A string without a#would run past the end.
Complexity
Time: O(N). encode appends each string once, plus a few characters for its length and the #. decode moves i and j forward only, so each character of the encoded string is looked at a constant number of times, and each payload is copied once by substr. The encoded string is longer than N by the length prefixes, a few characters per string, so the work is linear in the size of the encoded string.
Space: O(N) for the encoded string and for the decoded vector. Apart from the output, the methods use a handful of integers.
Edge cases and common mistakes
- An empty list. It encodes to
""and decodes to an empty vector, because thewhileloop never runs. - Empty strings.
[""]encodes to0#and["", ""]to0#0#. Each decodes to the right number of empty strings, since a length of 0 givessubstr(j + 1, 0), which is empty. - A
#or digits inside a string.["a#b", "12", "c"]works for the same reason["3#a", "", "#"]does: the decoder reads the length, then skips exactly that many characters without examining them. - Splitting on
#. Looking for the next#in the whole string, rather than only in the digits afteri, breaks as soon as a string contains a hash. - Leaving out the
#. Without the marker, lengths and string starts blend together when a string begins with a digit. - Forgetting
1 +. The next piece starts atj + 1 + length. Usingj + lengthlands one character too early, on the last character of the payload. - Cutting the string down. Calling
s = s.substr(...)after each piece copies the remaining text every round, which makes decoding quadratic.
Testing
These round trips all decode back to the input:
["lint", "code", "love", "you"]encodes to4#lint4#code4#love3#you.["3#a", "", "#"]encodes to3#3#a0#1##.[]encodes to the empty string and decodes to[].[""]encodes to0#.["", ""]encodes to0#0#.["hello world", "12", "a#b#c"]encodes to11#hello world2#125#a#b#c. Reading it back, the length11takeshello world, and the12in the data is not mistaken for a length.
