skip to Main Content

Here’s the problem:

Assume that the value of a = 1, b = 2, c = 3, … , z = 26. You are given a numeric string S. Write a program to return the list of all possible codes that can be generated from the given string.

So, 1123 will produce the following output:

aabc kbc alc aaw kw

So the way I’m approaching is this:

first map each alphabet to number, for e.g, "1":"a", "2":"b"... and so on.

Then recursively divide the problem:
f("123") = {alphabetMap("1")+ f("23"), alphabetMap("12") + f("3")}

So I draw the recursive tree with this approach and able to find the solution.

But somewhere there seems to be a problem when I implement the same.

var alphabetMap = {};
for(var i = 1; i <= 26; i++){
    alphabetMap[i] = String.fromCharCode(96 + i);
}
//Gives {"1": "a", "2": "b",........."26": "z"}

var res = [];
function encrypt(str, i = 0, newStr = ""){
    if(i >= str.length){
        res.push(newStr);
        return;
    }

    encrypt(str, i + 1, newStr + (alphabetMap[str[i]] || ''));
    encrypt(str, i + 2, newStr + (alphabetMap[str[i] + str[i + 1]] || ''));
}

encrypt("1123");
console.log(res)

Another issue I wanted to understand with this problem is how to intuitively think when there would be just one index needed to find all combination and when we’d need more.

For e.g, see this

Find all combinations of interleaving two strings. Where am I making mistake?

This problem is similar and the breakdown of the problem is similar too as with each step the problem is going to be divided into two. But we need two indices because we need two set of strings. Same argument can be applied here but here I think just once index suffices.

Appreciate insight on this.

3

Answers


  1. Your idea with recursion is fine, and yes, this only needs one index (since there is only one string to iterate). At each stage your function will try the two possibilities. But that is where you have a bug: before looking into the case where you take two digits, you should ensure that the string still has that many characters available.

    In that case + str[i + 1] will be undefined, and although you have provision for that case with || '' this means you make the recursive call with newStr itself (nothing appended to it), while still skipping the last character at i. In your example this means you output first aabc (correct), but then also aab, skipping the input "3" because there was no second character following it.

    The fix is to replace this:

    encrypt(str, i + 2, newStr + (alphabetMap[str[i] + str[i + 1]] || ''));
    

    with this:

    if (i + 2 <= str.length && alphabetMap[str[i] + str[i + 1]]){
        encrypt(str, i + 2, newStr + alphabetMap[str[i] + str[i + 1]]);
    }
    

    …so that you only apply the recursion when there are at least two digits, and that digit pair has a mapping. Now it will output the correct result. It is true you could rely on the undefined behaviour and leave out the length check, but the check on the existence of the mapping is a requirement for going into recursion. The call should not be made otherwise.

    Other remarks

    • Don’t use a global variable to collect the results. This is not good and would give you wrong results when you call the function multiple times from the main program.

    • I would not pass the partial code (newStr) as an argument to the recursive call. It seems more elegant to have the recursive call return the possible codes for the substring it received, and let the caller do the job of prefixing those with the current letter. That way it works from the inside-out (post order processing)

    • Passing the start index is fine, but JavaScript is quite efficient with slicing strings (using string kerning), so you could just pass the string slice instead of the original string. This reduces the number of arguments to just one.

    • Instead of building the result in such a way that the function returns the list of codes (which is way better than populating a global variable), you could also yield the codes using a generator.

    • You don’t really need the alphabet map, as it is straightforward to do the decoding directly with String.fromCharCode.

    Here is how that would turn out:

    function* encryptIter(str) {
        if (!str) return yield "";
        for (let len = 1; len <= 2; len++) {
            const code = String.fromCharCode(96 + +str.slice(0, len));
            if (str.length >= len && code <= "z") {
                for (const result of encrypt(str.slice(len))) {
                    // Prefix the current code to the results for the shorter string
                    yield code + result;
                }
            }
        }
    }
    
    const encrypt = (str) => [...encryptIter(str)];
    
    const res = encrypt("1123");
    console.log(res);
    Login or Signup to reply.
  2. Here’s an approach with generators. I find yield expressions allow me to think about the program from a higher level. Another advantage is the generator is pauseable/cancelable so that you do not need to generate the entire result set if it’s not needed.

    const alphabet = Array.from(
      Array(26),
      (value, index) => String.fromCharCode(96 + index),
    )
    
    function *decode(s) {
      if (s == "")
        yield ""
      
      if (s.length >= 1)
        for (const plain of decode(s.slice(1)))
          yield *combine(s.slice(0, 1), plain)
    
      if (s.length >= 2)
        for (const plain of decode(s.slice(2)))
          yield *combine(s.slice(0, 2), plain)
    }
    
    function *combine(s, plain) {
      if (s in alphabet)
        yield alphabet[s] + plain
    }
    
    for (const res of decode("1123"))
      console.log(res)
    aabc
    aaw
    alc
    kbc
    kw
    

    This code is nicely arranged to handle errors. Update the combine function to –

    function *combine(s, plain) {
      if (s in alphabet)
        yield alphabet[s] + plain
      else
        yield `[ERROR: ${s}]` + plain // ✅
    }
    

    Now when the input is invalid –

    for (const res of decode("112a3"))
      console.log(res)
    
    aab[ERROR: a]c
    aab[ERROR: a3]
    aa[ERROR: 2a]c
    al[ERROR: a]c
    al[ERROR: a3]
    kb[ERROR: a]c
    kb[ERROR: a3]
    k[ERROR: 2a]c
    
    
    Login or Signup to reply.
  3. Here’s a more generic non-recursive approach (basically, BFS the tree instead of DFS).

    The function expects a Map prefix => code and a source string. Prefixes and codes can be whatever, not necessary digits or letters.

    const prefixes = new Map([
        ['1', 'a'],
        ['2', 'b'],
        ['3', 'c'],
        ['4', 'd'],
        ['5', 'e'],
        ['6', 'f'],
        ['7', 'g'],
        ['8', 'h'],
        ['9', 'i'],
        ['10', 'j'],
        ['11', 'k'],
        ['12', 'l'],
        ['13', 'm'],
        ['14', 'n'],
        ['15', 'o'],
        ['16', 'p'],
        ['17', 'q'],
        ['18', 'r'],
        ['19', 's'],
        ['20', 't'],
        ['21', 'u'],
        ['22', 'v'],
        ['23', 'w'],
        ['24', 'x'],
        ['25', 'y'],
        ['26', 'z'],
    ])
    
    function encode(prefixes, str) {
        let res = [],
            queue = [['', str]]
    
        while (queue.length > 0) {
            let [encoded, notEncoded] = queue.shift()
            if (notEncoded === '')
                res.push(encoded)
            else {
                for (let [pfx, code] of prefixes.entries()) {
                    if (notEncoded.indexOf(pfx) === 0) {
                        queue.push([encoded + code, notEncoded.slice(pfx.length)])
                    }
                }
            }
        }
    
        return res
    }
    
    console.log(...encode(prefixes, '1123'))
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search