Start learning free

LESSON 322 OF 540 · HASH TABLE

Encode and Decode Strings in Kotlin: 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

    KOTLIN · HASH TABLE

    Encode And Decode Strings in Kotlin, explained

    Problem overview

    The class Codec has two methods. encode receives a List<String> and returns a single String. decode receives that string, on what the tests treat as a different machine with a different Codec object, and must return the original list in the same order. Nothing is shared between the two calls except the string itself.

    The strings can hold any printable character, including digits, spaces and #, and they can be empty. For ["lint", "code", "love", "you"] the encoded string is 4#lint4#code4#love3#you, and decoding it gives the same four strings back. For ["3#a", "", "#"] it is 3#3#a0#1##, and decoding gives the same three strings again. An empty list encodes to the empty string, and [""] encodes to 0#, so those two inputs stay apart.

    Approach: say how long each string is

    The obvious idea is to join the strings with a separator such as , or # and split on it when decoding. It fails because any separator character can also appear inside a string, and then decode cuts in the wrong place. Escaping the separator inside the strings works, but it makes both methods more complicated.

    A length prefix avoids the problem. Before each string, write how many characters it has, then a #, then the string itself. When decoding, read the digits up to the first #, convert them to a number length, and take exactly length characters after the #. Those characters are never inspected, so they may contain anything. The next piece starts right after them.

    The # after the length is still needed. Without it, 12 followed by a string that starts with a digit could be read as a longer length. Because a length is only digits, the first # reached while reading a length is always the one that encode wrote.

    Both methods should be linear in N, the total length of the strings. Two habits keep them that way: build the output with a StringBuilder instead of adding to a String in a loop, and walk through the encoded string with an index instead of cutting it into shorter and shorter copies.

    Step by step

    encode

    1. Create a StringBuilder named encoded. Appending to it adds text to the end of one growing buffer. Using += on a String would build a brand new string each time, and the repeated copying adds up.
    2. Loop over the strings with for (s in strs).
    3. Append the three pieces for each string: s.length, the character '#' and s. append returns the builder itself, so the calls chain on one line.
    4. Return encoded.toString() after the loop. With an empty list the loop never runs and the result is the empty string.

    decode

    1. Create the result list strs, an empty ArrayList<String>(), and an index i set to 0. The index marks where the next piece starts.
    2. Loop while i < s.length. Each round of the loop reads exactly one piece. An empty input skips the loop and gives an empty list.
    3. Find the #. Start j at i and add 1 to it while s[j] is not '#'. The characters from i up to j are the length's digits.
    4. Convert the digits with s.substring(i, j).toInt() and store the number in length.
    5. Take the string with s.substring(j + 1, j + 1 + length) and add it to strs. The end index is exclusive, so this takes exactly length characters.
    6. Move i to j + 1 + length, where the next length begins, and return strs after the loop.

    The solution in Kotlin

    class Codec {
        fun encode(strs: List<String>): String {
            val encoded = StringBuilder()
            for (s in strs) {
                encoded.append(s.length).append('#').append(s)
            }
            return encoded.toString()
        }
    
        fun decode(s: String): List<String> {
            val strs = ArrayList<String>()
            var i = 0
            while (i < s.length) {
                var j = i
                while (s[j] != '#') {
                    j += 1
                }
                val length = s.substring(i, j).toInt()
                strs.add(s.substring(j + 1, j + 1 + length))
                i = j + 1 + length
            }
            return strs
        }
    }
    

    The Kotlin details worth a closer look:

    • append accepts several types. StringBuilder.append(Int) writes the decimal digits, so append(s.length) adds 4 for a four-character string. append('#') takes a Char and append(s) takes a String.
    • Characters and strings differ. s[j] is a Char, so it is compared with '#' in single quotes. Comparing it with the string "#" would not compile.
    • substring(start, end) includes start and excludes end. When length is 0, j + 1 and j + 1 + length are equal, and the result is the empty string, which is how an empty element survives the round trip.
    • toInt() parses the digits. It would throw a NumberFormatException if it got anything else, but decode only receives what encode made.
    • var and val. i and j change, so they are var. length is set once per round, so it is val.

    Complexity

    Time: O(N) for both methods. encode appends each string once, and the builder copies each character into its buffer a constant number of times on average. decode moves i forward past every character once. Scanning for # reads only the digits of the length, and substring copies each string once.

    Space: O(N). The encoded string and the decoded list each hold about N characters, plus a few characters of length prefix per string.

    Edge cases and common mistakes

    • Strings that look like the format. ["3#a", "", "#"] round-trips correctly, because decode trusts the length and never looks at what is inside the string.
    • Empty list versus a list with one empty string. [] encodes to "" and [""] encodes to "0#", so decode returns two different results.
    • Several empty strings. ["", ""] encodes to 0#0# and decodes to two empty strings.
    • Multi-digit lengths. A string of 12 characters gets the prefix 12#. Reading only one digit, or assuming one-digit lengths, breaks as soon as a string reaches 10 characters.
    • Leaving out the #. Without it, the length of the next string could run into the digits that start the previous string.
    • Cutting s in pieces. Replacing s by s.substring(...) in each round copies the remaining text every time, which can turn O(N) into O(N * k) for k strings.

    Testing

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