Start learning free

LESSON 322 OF 540 · HASH TABLE

Encode and Decode Strings in TypeScript: Length Prefixes in O(N)

Practice O(n) Read the full explanation

Design a way to turn a list of strings into one string and back. One program encodes the list and sends the string to another program, which decodes it into the same list of strings, in the same order.

Implement the class Codec:

  • encode(strs) gets a list of strings strs and returns one string that holds all of them.
  • decode(s) gets a string s made by encode and returns the original list of strings.

The strings can contain any printable character, including #, digits and spaces, and a string can be empty. So no single character can mark where one string ends and the next begins: that character could be inside a string too.

Each test is one list. It is encoded with one Codec object and decoded with another, as if the string were sent to another machine, so decode can use only the string it gets. The output is the decoded list. The starter code spells the method names the way your language does.

 

Example 1:

Input: strs = ["lint","code","love","you"]
Output: ["lint","code","love","you"]
Explanation: encode can return "4#lint4#code4#love3#you": for each string, its length,
a #, then the string itself. decode reads the 4 before the first #, takes the 4 characters
after it, "lint", and goes on from there.

Example 2:

Input: strs = ["3#a","","#"]
Output: ["3#a","","#"]
Explanation: The encoded string is "3#3#a0#1##". decode reads the length 3 up to the first #,
then takes exactly 3 characters, "3#a", although they hold a digit and a #.
Then "0#" gives the empty string, and "1##" gives "#".

Example 3:

Input: strs = []
Output: []
Explanation: An empty list encodes to the empty string "", which decodes to no strings at all.
A list with one empty string, [""], encodes to "0#" instead, so the two stay different.

 

Constraints:

  • 0 <= strs.length <= 105
  • 0 <= strs[i].length <= 200
  • The total length of all the strings is at most 106.
  • strs[i] consists of printable ASCII characters (codes 32 to 126): letters, digits, spaces and punctuation such as #.

Note:

Both encode and decode should run in O(N) time, where N is the total length of all the strings.

Write each string's length in front of it, followed by a #: <length>#<string>. To decode, read the digits up to the next #, then take exactly that many characters, whatever they are; the next string starts right after them.

Keep both methods linear: don't copy the whole encoded string again for every string you add, and don't cut s into shorter and shorter copies while decoding; move an index through it instead.

STEP 1 OF 14
    Ctrl + Enter to check

    TYPESCRIPT · HASH TABLE

    Encode And Decode Strings in TypeScript, explained

    Problem overview

    The class Codec has two methods. encode(strs) turns an array of strings into a single string, and decode(s) turns that string back into the original array, in the same order. The two methods run on different Codec objects, as if the string had travelled to another machine, so decode may use only the string it receives and nothing remembered by encode.

    The strings may hold any printable character, including #, digits and spaces, and a string may 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 gives the same three strings back. Both methods must take time proportional to the total length of the strings.

    Approach: write each length before its string

    The obvious idea is to join the strings with a separator and split on it later. It fails as soon as a string can contain the separator. With "#" as the separator, ["a#b", "c"] becomes "a#b#c", and splitting it gives ['a', 'b', 'c']: three strings instead of two. Because every printable character may appear inside a string, no single character can be a safe separator.

    The fix is to stop looking for the end of a string and to say where it ends instead. Before each string, write its length, then a #, then the string itself: <length>#<string>. To decode, read the digits up to the next #, then take exactly that many characters, whatever they are. The next piece starts right after them. The decoder never looks inside a string, so a # or a digit in the content can't confuse it.

    The # after the length is still needed. It marks where the digits end, so a string that starts with a digit doesn't run into its own length: 12 followed by 345 would be ambiguous without it. A length is made only of digits, so the first # after the start of a piece is always the one encode wrote.

    The empty list and the list holding one empty string stay different: [] encodes to "", while [""] encodes to "0#".

    Step by step

    encode

    1. Start with an empty string. Declare encoded with let, since it grows.
    2. Loop over the strings with for...of, taking each one as s.
    3. Append a piece for each string: its length, a "#", then the string. Adding a number to a string with + turns the number into its digits, so s.length + "#" is text such as "5#".
    4. Return encoded after the loop.

    decode

    1. Prepare the result: a constant strs of type string[], empty at first.
    2. Keep an index i, starting at 0, for the place where the next piece begins.
    3. Loop while i < s.length. Each round reads one piece.
    4. Find the #. Start j at i and move it forward while s[j] isn't "#".
    5. Read the length. s.substring(i, j) holds the digits, and Number converts them.
    6. Take the string: exactly length characters starting right after the #, that is, substring(j + 1, j + 1 + length). The end index is exclusive. Push it onto strs.
    7. Move i to j + 1 + length, where the next piece begins.
    8. Return strs.

    The solution in TypeScript

    class Codec {
        encode(strs: string[]): string {
            let encoded = "";
            for (const s of strs) {
                encoded += s.length + "#" + s;
            }
            return encoded;
        }
    
        decode(s: string): string[] {
            const strs: string[] = [];
            let i = 0;
            while (i < s.length) {
                let j = i;
                while (s[j] !== "#") {
                    j++;
                }
                const length = Number(s.substring(i, j));
                strs.push(s.substring(j + 1, j + 1 + length));
                i = j + 1 + length;
            }
            return strs;
        }
    }
    

    Some details worth knowing:

    • Number plus string. In s.length + "#" + s, the first + has a number on the left and a string on the right, so it concatenates: 5 + "#" is "5#". The whole expression has type string, which matches encoded.
    • Indexing a string. s[j] is a one-character string, so comparing it with "#" using !== is a plain text comparison. Indexing past the end gives undefined, not an error. The loop never gets there on valid input, because every length is followed by a #.
    • substring. substring(start, end) takes the characters from start up to, but not including, end. Passing the same number twice gives an empty string, which is how an empty entry such as "0#" decodes.
    • Types are only notes. string[] and : string are erased when the code is compiled to JavaScript. They help the compiler check your code, and they don't validate the string that decode receives.

    Complexity

    Time: O(N), where N is the total length of all the strings. encode adds each string once, and the loop does a constant amount of work around each one. In decode, the inner loop moves j over the digits of a length, and substring copies the characters of one string. Each character of s is passed over a constant number of times, and i only moves forward. Cutting s into shorter and shorter copies in every round would copy the rest of the string each time and slow decoding down, which is why the code moves an index instead.

    Space: O(N) for the encoded string and for the decoded array, apart from the input itself. Beyond that, decode uses a few integers.

    Edge cases and common mistakes

    • Empty list. encode([]) returns "", and decode("") never enters the loop, so it returns [].
    • Empty strings. [""] encodes to "0#" and decodes to one empty string. ["", ""] encodes to "0#0#" and decodes to two.
    • A # or digits inside a string. ["3#a","","#"] works because the decoder trusts the length: it reads 3, then takes 3#a as three characters.
    • Forgetting the # after the length. Without it, the digits of a length and the digits at the start of a string blur together.
    • Using a fixed-width length. A one-digit length would break for long strings. The # allows a length of any number of digits.
    • Off-by-one in the slice. The string starts at j + 1, just after the #, not at j.
    • Counting the wrong thing. The length is the number of characters in the string, not including the prefix.

    Testing

    • ["lint","code","love","you"] encodes to "4#lint4#code4#love3#you" and decodes back to the same array.
    • ["3#a","","#"] encodes to "3#3#a0#1##" and decodes back to the same array.
    • [] encodes to "" and decodes to [].
    • [""] encodes to "0#" and decodes to [""].
    • ["a b", "12", "#3#"] encodes to "3#a b2#123##3#" and decodes back to the same array.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free