Problem overview
The Codec struct has two methods. encode takes a Vec<String> and returns a single String. decode takes a string made by encode and returns the original vector, with the same strings in the same order. The two calls happen on different Codec objects, so decode can rely on nothing but the string it receives.
The strings can hold any printable ASCII character, including digits, spaces and #, and a string can be empty. For ["lint", "code", "love", "you"], one valid encoding is "4#lint4#code4#love3#you", and decoding it gives the four words back. An empty vector must come back as an empty vector, and [""] must come back as a vector holding one empty string.
Approach: write each length first
The obvious idea is to put a separator between the strings, but any character you pick can also appear inside a string. With # as the separator, ["a#b"] and ["a", "b"] would both become a#b, and nothing could tell them apart.
The fix is to stop searching for the end of a string and say where it ends instead. Before each string, write its length and a #. The decoder reads the digits up to the first #, converts them to a number, and takes exactly that many characters, whatever they are. It never looks inside the string, so a # or a digit in the middle can't confuse it.
The # after the length has a job too: without it, a string that starts with a digit would run into its own length. "3" followed by "12" must not be readable as "31" followed by "2". Because the digits come first and the # ends them, the first # after a piece starts is always the one encode wrote.
The trickiest input is ["3#a", "", "#"]. It encodes to "3#3#a0#1##". Decoding goes like this:
start i | first # at j | length | piece taken | next i |
|---|---|---|---|---|
| 0 | 1 | 3 | "3#a" | 5 |
| 5 | 6 | 0 | "" | 7 |
| 7 | 8 | 1 | "#" | 10 |
The first piece holds a digit and a #, and it is still taken whole, because the decoder counted three characters instead of looking for a marker. At i = 10 the string is used up and the loop ends.
Both methods should take time proportional to the total length of the strings, N. That rules out two tempting shortcuts: copying the whole encoded string again for every string you add, and cutting the input into shorter and shorter copies while decoding. The solution appends to one String and moves an index through the input.
Step by step
encode
- Create the result.
encodedis alet mutemptyStringthat every piece will be appended to. - Loop over
&strs. Borrowing the vector means the strings stay where they are instead of being moved out of it. - Append one piece per string.
format!fills its{}placeholders in order: the length, a#, then the string.push_strtakes a&str, so the piece is passed with&. - Return
encodedwith no semicolon, so it is the function's value.
decode
- Take the bytes.
s.as_bytes()gives a&[u8], where any position is read in one step. - Create the result and the index.
strsstarts as an emptyVec<String>, andi, the start of the next piece, starts at 0. - Loop while
i < bytes.len(). Each round reads one piece. An empty input never enters the loop. - Find the
#. Startjatiand move it forward untilbytes[j]isb'#', the byte value of the character#. The bytes betweeniandjare the digits of the length. - Parse the length.
s[i..j].parse().unwrap()needs the annotationlength: usize, becauseparsecan produce many kinds of numbers and must be told which one. - Take the string. The
lengthbytes after the#run fromj + 1up to, but not including,j + 1 + length.to_string()copies that slice into an ownedStringfor the vector. - Move the index to
j + 1 + length, where the next piece starts. - Return
strs.
The solution in Rust
struct Codec {}
impl Codec {
fn new() -> Self {
Codec {}
}
fn encode(&self, strs: Vec<String>) -> String {
let mut encoded = String::new();
for s in &strs {
encoded.push_str(&format!("{}#{}", s.len(), s));
}
encoded
}
fn decode(&self, s: String) -> Vec<String> {
let bytes = s.as_bytes();
let mut strs: Vec<String> = Vec::new();
let mut i = 0;
while i < bytes.len() {
let mut j = i;
while bytes[j] != b'#' {
j += 1;
}
let length: usize = s[i..j].parse().unwrap();
strs.push(s[j + 1..j + 1 + length].to_string());
i = j + 1 + length;
}
strs
}
}
Rust details worth knowing:
len()counts bytes. For printable ASCII, one character is one byte, so the length written byencodeand the length used bydecodeagree."é".len()is 2, and the slicing indecodealso works in bytes. That is fine here, because the lesson only uses ASCII.- Slices of a
Stringuse byte positions.s[i..j]panics if either end falls inside a multi-byte character. With ASCII input every position is a boundary, so this never happens. b'#'is au8. Iterating over bytes means comparing with a byte literal, not with thechar'#'.unwrappanics on bad input.decodetrusts that its argument came fromencode. A string such as"x#"would panic atparse, and a string with no#would runjpast the end ofbytes. For this lesson that is acceptable; a real protocol would return an error instead.- The temporary from
format!. Each piece is a smallStringthat is copied intoencodedand then dropped. That work is proportional to the piece, so the total stays linear. - Last expressions.
encodedandstrshave no semicolon, which makes them the values the methods return.
Complexity
Time: O(N) for each method, where N is the total length of the strings. encode copies each string once, plus a few digits for its length. decode moves j over each length prefix and copies each string once, and i only ever moves forward. With 100,000 strings of two or three characters each, the encoded text is 490,000 bytes long and decodes back to the same vector.
Space: O(N) for the output of each method. No extra structure is needed beyond a few integers.
Edge cases and common mistakes
- Empty list against a list with one empty string.
[]encodes to""and decodes to[].[""]encodes to"0#"and decodes to a vector with one empty string. A scheme with no length-and-#for empty strings would merge the two. - Strings that look like the encoding.
["10#abc", "#", "12"]encodes to"6#10#abc1##2#12"and decodes correctly, because the decoder counts characters instead of interpreting what it reads. - Dropping the
#. Writing only the length makes the boundary between a length and the string after it ambiguous. - Looking for the
#from the wrong place. If the search for#started inside the string instead of ati, a#in the content would be mistaken for the marker. - Off-by-one in the slice. The string starts at
j + 1, after the#, not atj. - Cutting the input down. Replacing
sby the remainder after every piece copies the rest of the string each time, and makes decoding quadratic.
Testing
["lint", "code", "love", "you"]encodes to"4#lint4#code4#love3#you"and decodes back to the same four strings.["3#a", "", "#"]encodes to"3#3#a0#1##"and decodes back to the same three strings.[]encodes to""and decodes to[].[""]encodes to"0#"and decodes to[""].["", ""]encodes to"0#0#"and decodes to two empty strings.
