Encode and Decode Strings
Write each word as its length, a marker, then the word, so the decoder always knows where a word ends, whatever characters it holds. Solved with a length prefix in O(L) time, with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”Write two functions. encode takes a list of strings and turns it into one string. decode
takes that one string and gives back the original list, exactly as it was.
The strings can hold any ASCII character, including spaces, digits, # and every other symbol. A
string can also be empty. decode(encode(words)) must always equal words.
Example 1
Input: words = ["hi", "a#b", ""]
Output: ["hi", "a#b", ""]
One valid encoding is “2#hi3#a#b0#”. Decoding it gives back all three words, the empty one too.
Example 2
Input: words = ["4#x"]
Output: ["4#x"]
The word itself looks like a length prefix. It is encoded as “3#4#x”, and the decoder still reads it correctly.
Example 3
Input: words = []
Output: []
An empty list encodes to the empty string, and that decodes back to an empty list.
Constraints
Section titled “Constraints”- 0 ≤ number of strings ≤ 200
- 0 ≤ words[i].length ≤ 200
- words[i] holds any of the 256 ASCII characters.
- Below, L is the total length of all the strings.
Clarifying questions
Section titled “Clarifying questions”- Can a string contain the separator I pick?
- Yes. Any character can appear, so no separator is safe on its own.
- Can a string be empty?
- Yes, and the list can be empty too. Both must come back exactly.
- Does the encoded string need a fixed format?
- No. Any format works, as long as your decode reads what your encode wrote.
- May decode keep state from encode?
- No. Decode must work from the string alone, as if it ran on another machine.
Spot the pattern
Section titled “Spot the pattern”Joining words with a separator fails as soon as a word contains that separator. The fix is to stop looking for the end of a word and say in advance how long it is. This is how many network protocols frame messages: a length, then exactly that many bytes.
Solutions
Section titled “Solutions”This problem has one approach worth showing. Joining with a plain separator is broken, and escaping special characters works but is longer and easy to get wrong.
Length prefix O(L)
The idea
Section titled “The idea”Encode: for each word, write its length, then a #, then the word. So “hi” becomes “2#hi” and
“a#b” becomes “3#a#b”.
Decode: start at position i = 0. Read digits up to the next #. Those digits are the length
size. The word is then exactly the next size characters, whatever they are. Take them, jump i
past them, and repeat until the end.
The # only has to mark where the digits end, and digits never contain a #. Inside a word, a
# is never searched for, because the decoder jumps over the word by its length.
Why it is correct: at every step, i points at the start of a length. The length tells the
decoder exactly where the word ends, so the next i is again the start of a length.
Walkthrough
Section titled “Walkthrough”- def encode(words):
- encoded = ""
- for word in words:
- encoded += str(len(word)) + "#" + word
- return encoded
- def decode(encoded):
- words, i = [], 0
- while i < len(encoded):
- hash_index = encoded.index("#", i)
- size = int(encoded[i:hash_index])
- words.append(encoded[hash_index + 1 : hash_index + 1 + size])
- i = hash_index + 1 + size
- return words
Encode: write each word as its length, a #, then the word itself. Start with an empty string.
// Length prefix: write each word as "<length>#<word>".// Every character is written once and read once, so O(L) time and memory,// where L is the total length of all words.class Codec { public String encode(List<String> words) { // A StringBuilder keeps building the string linear. StringBuilder encoded = new StringBuilder(); for (String word : words) encoded.append(word.length()).append('#').append(word); return encoded.toString(); }
public List<String> decode(String encoded) { List<String> words = new ArrayList<>(); int i = 0; while (i < encoded.length()) { // The first # after i always ends the length: digits never contain #. int hashIndex = encoded.indexOf('#', i); int size = Integer.parseInt(encoded.substring(i, hashIndex)); // Take exactly size characters, so a # inside the word is harmless. words.add(encoded.substring(hashIndex + 1, hashIndex + 1 + size)); i = hashIndex + 1 + size; } return words; }}# Length prefix: write each word as "<length>#<word>".# Every character is written once and read once, so O(L) time and memory,# where L is the total length of all words.class Codec: def encode(self, words: list[str]) -> str: # Collect the pieces and join once, so building the string stays linear. parts = [] for word in words: parts.append(str(len(word)) + "#" + word) return "".join(parts)
def decode(self, encoded: str) -> list[str]: words, i = [], 0 while i < len(encoded): # The first # after i always ends the length: digits never contain #. hash_index = encoded.index("#", i) size = int(encoded[i:hash_index]) # Take exactly size characters, so a # inside the word is harmless. words.append(encoded[hash_index + 1 : hash_index + 1 + size]) i = hash_index + 1 + size return words// Length prefix: write each word as "<length>#<word>".// Every character is written once and read once, so O(L) time and memory,// where L is the total length of all words.public class Codec{ public string Encode(IList<string> words) { // A StringBuilder keeps building the string linear. var encoded = new System.Text.StringBuilder(); foreach (string word in words) encoded.Append(word.Length).Append('#').Append(word); return encoded.ToString(); }
public IList<string> Decode(string encoded) { var words = new List<string>(); int i = 0; while (i < encoded.Length) { // The first # after i always ends the length: digits never contain #. int hashIndex = encoded.IndexOf('#', i); int size = int.Parse(encoded.Substring(i, hashIndex - i)); // Take exactly size characters, so a # inside the word is harmless. words.Add(encoded.Substring(hashIndex + 1, size)); i = hashIndex + 1 + size; } return words; }}// Length prefix: write each word as "<length>#<word>".// Every character is written once and read once, so O(L) time and memory,// where L is the total length of all words.type Codec struct{}
func (codec *Codec) Encode(words []string) string { // A Builder keeps building the string linear. var encoded strings.Builder for _, word := range words { encoded.WriteString(strconv.Itoa(len(word))) encoded.WriteByte('#') encoded.WriteString(word) } return encoded.String()}
func (codec *Codec) Decode(encoded string) []string { words := []string{} i := 0 for i < len(encoded) { // The first # after i always ends the length: digits never contain #. hashIndex := i + strings.IndexByte(encoded[i:], '#') size, _ := strconv.Atoi(encoded[i:hashIndex]) // Take exactly size characters, so a # inside the word is harmless. words = append(words, encoded[hashIndex+1:hashIndex+1+size]) i = hashIndex + 1 + size } return words}// Length prefix: write each word as "<length>#<word>".// Every character is written once and read once, so O(L) time and memory,// where L is the total length of all words.function encode(words: string[]): string { // Collect the pieces and join once, so building the string stays linear. const parts: string[] = []; for (const word of words) parts.push(`${word.length}#${word}`); return parts.join("");}
function decode(encoded: string): string[] { const words: string[] = []; let i = 0; while (i < encoded.length) { // The first # after i always ends the length: digits never contain #. const hashIndex = encoded.indexOf("#", i); const size = Number(encoded.slice(i, hashIndex)); // Take exactly size characters, so a # inside the word is harmless. words.push(encoded.slice(hashIndex + 1, hashIndex + 1 + size)); i = hashIndex + 1 + size; } return words;}1 Find the work that repeats
In
encode, each run of the loop writes one word with its prefix. Indecode, each run of the loop reads one prefix and copies one word. The work in one run is not 1 step: it is the length of that word plus its few prefix characters. So count characters, not runs.- def encode(words):
- encoded = ""
- for word in words:
- encoded += str(len(word)) + "#" + word
- return encoded
- def decode(encoded):
- words, i = [], 0
- while i < len(encoded):
- hash_index = encoded.index("#", i)
- size = int(encoded[i:hash_index])
- words.append(encoded[hash_index + 1 : hash_index + 1 + size])
- i = hash_index + 1 + size
- return words
2 Count the runs on Example 1
The encoded string in Example 1 is “2#hi3#a#b0#”.
decodereads 4 characters for “2#hi”, 5 for “3#a#b” and 2 for “0#”.4 + 5 + 2 = 11 characters, each read once. That is the whole string, no more.
word letters prefix characters read "hi" 2 2# 4 "a#b" 3 3# 5 "" 0 0# 2 total4 + 5 + 2 = 11 = len(encoded)3 Write the count for any n
For each word, the work is its length plus its prefix: a few digits (at most 3, since a word has at most 200 characters) and one
#. With w words of total length L:L + w × (digits + 1) ≤ L + 4wCheck it with Example 1: 5 letters + 3 words × 2 = 11.
4 Keep only the part that grows fastest
- Drop the 4w. Each word adds at most 4 characters of prefix, a fixed amount per word. When the words get longer, L takes over.
What is left is L.
encodeis the same count, since it writes whatdecodereads.So the time is O(L).
S Space: count the extra memory the same way
encodebuilds one string of about L characters.decodebuilds a list that holds all L characters again. The space is O(L), which the output needs anyway.i,hash_indexandsizeare a fixed few numbers.encoded (L characters)words (L characters)ihash_indexsize
Tested at the limits
Section titled “Tested at the limits”2,000 random lists of 0 to 5 words, mostly made of # and digits, so words like 3# and 12#ab that look like a length prefix come up often. Empty lists and empty words too. Decoding the encoded text gave back the exact same words every time.
| Largest inputs | Result |
|---|---|
| 200 words of up to 200 characters (the most allowed), any ASCII character, plus one word that is 9# repeated 100 times. Length prefix. | Under 0.001 s, and about 0.05 MB of extra memory: the encoded text and the decoded words, a few copies of L. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.