Skip to content
← All posts

Decrypt String from Alphabet to Integer Mapping

5 min read
leetcodeproblemeasystrings

Given a string s formed by digits and #, return the string formed after the following mapping: characters 1 through 9 map to 'a' through 'i', and characters 10# through 26# map to 'j' through 'z'.

This is LeetCode 1309: Decrypt String from Alphabet to Integer Mapping, and it is a great introduction to parsing strings where different tokens have different lengths. The # character acts as a signal that the preceding two digits should be read together as a single number.

encoded string10# = 'j'11# = 'k'1 = 'a'2 = 'b'10#11#12decoded resultjkab
Blue groups: two-digit numbers followed by #, mapped to letters j-z. Green cells: single digits mapped to letters a-i.

Why this problem matters

String parsing shows up everywhere, from compilers to network protocols to configuration files. The core skill is recognizing how to decide what chunk of input to consume next. In this problem, the # tells you whether to grab one character or three. That same pattern of "look ahead (or behind) to decide the token boundary" appears in problems involving Roman numerals, run-length encoding, nested structures, and real-world format parsers.

Once you internalize this kind of conditional scanning, you will find it applies far beyond LeetCode.

The key insight

Scan the string from right to left. If the current character is #, the two characters immediately before it form a two-digit number (10 through 26), which maps to 'j' through 'z'. If the current character is not #, it is a single digit (1 through 9) mapping to 'a' through 'i'.

Scanning right-to-left makes the decision trivial: you always know at the current position whether you are looking at a # marker or a plain digit. You never have to peek ahead. Collect the decoded characters in reverse order and then reverse at the end, or use a stack.

You can also scan left-to-right by peeking two positions ahead to check for #, but right-to-left feels more natural since the # sits at the end of each group.

The solution

def freq_alphabets(s: str) -> str:
    result = []
    i = len(s) - 1

    while i >= 0:
        if s[i] == '#':
            num = int(s[i - 2:i])
            result.append(chr(ord('a') + num - 1))
            i -= 3
        else:
            num = int(s[i])
            result.append(chr(ord('a') + num - 1))
            i -= 1

    return ''.join(reversed(result))

We start a pointer i at the last index of the string. On each iteration, we check if the character at i is #. If it is, we slice out the two-digit number from positions i - 2 to i (exclusive), convert it to the corresponding letter, and move i back by 3. If the character is a plain digit, we convert that single digit to a letter and move i back by 1.

Because we are collecting letters in reverse, we call reversed at the end to produce the correct order.

Scanning right-to-left avoids any ambiguity. When you see #, you know exactly which two digits it belongs to. When you see a digit without # at your current position, it is always a single-character token. No lookahead required.

Visual walkthrough

Let's trace through the input "10#11#12" step by step. We start at the rightmost character and work backward, deciding at each position whether we have a single digit or a two-digit-plus-hash group.

Step 1: Start at index 7 (rightmost). No # at index 9 (out of bounds). Single digit: 2 maps to 'b'.

input10#11#12decoded so farb

i = 7. Character is '2'. No '#' two positions ahead. chr(ord('a') + 2 - 1) = 'b'. Move i to 6.

Step 2: Index 6. No # at index 8. Single digit: 1 maps to 'a'.

input10#11#12decoded so farab

i = 6. Character is '1'. chr(ord('a') + 1 - 1) = 'a'. Move i to 5.

Step 3: Index 5 is '#'. Take two digits before it: s[3..4] = '11'. 11 maps to 'k'.

input10#11#12decoded so farkab

i = 5. Found '#'. Number = int('11') = 11. chr(ord('a') + 11 - 1) = 'k'. Move i to 2.

Step 4: Index 2 is '#'. Take two digits before it: s[0..1] = '10'. 10 maps to 'j'.

input10#11#12decoded so farjkab

i = 2. Found '#'. Number = int('10') = 10. chr(ord('a') + 10 - 1) = 'j'. Move i to -1.

Step 5: i is below 0. Done. Reverse the collected characters.

input10#11#12decoded so farjkab

We collected [b, a, k, j] right-to-left. Reversed: "jkab". That is the final answer.

Notice how every decision is local. At each position, you look at the current character: if it is #, grab three characters and step back three; if it is a digit, grab one and step back one. There is never a need to scan forward or backtrack.

Complexity analysis

ApproachTimeSpace
Right-to-left scanO(n)O(n)

Time is O(n) where n is the length of the input string. Each character is visited exactly once. The pointer moves by 1 or 3 on each step, but the total movement across all steps adds up to n.

Space is O(n) for the result list. We store one decoded character for each token in the input. In the worst case (all single digits), that is n characters. In the best case (all two-digit groups), it is roughly n/3 characters. Either way, it is proportional to n.

The building blocks

1. Right-to-left token scanning

i = len(s) - 1
while i >= 0:
    if s[i] == '#':
        token = s[i - 2:i]
        i -= 3
    else:
        token = s[i]
        i -= 1

This pattern of scanning backward and using a sentinel character (#) to decide the token length is the core technique. You will see variations of it whenever a string encodes variable-length tokens with delimiters. The key is that the delimiter tells you unambiguously how many characters belong to the current token.

2. Character mapping via arithmetic

chr(ord('a') + num - 1)

Instead of building a lookup dictionary from 1 to 26, you can compute the character directly. ord('a') gives you the ASCII value of 'a', and adding num - 1 shifts to the right letter. This works because the letters a through z are contiguous in ASCII. The same trick applies to Roman numeral problems, Caesar ciphers, and any mapping between sequential numbers and letters.

Edge cases

  • All single digits: "12345" has no # characters. Every digit is a standalone token mapping to 'a' through 'e'. The pointer moves by 1 each step.
  • All two-digit groups: "10#11#12#" is entirely composed of hash-terminated groups. The pointer always moves by 3.
  • Mixed tokens: "10#11#12" (our example) has two hash groups followed by two single digits. The algorithm handles the transition seamlessly.
  • Single character: "1" maps to "a". Minimal input, one iteration.
  • Maximum values: "26#" maps to "z". "9" maps to "i". These are the boundary values for each token type.
  • Long string with repeated patterns: "1326#" maps to "a", "c", "z". The algorithm correctly segments despite digits that could be read multiple ways left-to-right.

From understanding to recall

You now know how to parse a string with variable-length tokens by scanning right-to-left and using a delimiter to decide the token boundary. The code is short, the logic is clean, and the time complexity is optimal. But can you write it from memory in five minutes?

The details that catch people off guard are small. Do you slice s[i-2:i] or s[i-2:i+1]? Do you subtract 3 or 2 from i after reading a hash group? Do you reverse the result or build it left-to-right? These are not conceptual misunderstandings. They are recall gaps, and they show up under pressure.

Spaced repetition closes them. You practice writing the right-to-left scanner and the character mapping from memory at increasing intervals. After a few rounds, the token boundary logic becomes automatic. You see "decode this encoded string" in a problem statement, and the scanning template flows out without hesitation.

Related posts

  • Valid Palindrome - Another string parsing problem where you process characters with specific rules
  • Decode Ways - Similar digit-to-letter mapping problem with a dynamic programming twist
  • Roman to Integer - Another string-to-number decoding problem that uses look-ahead to handle variable-length tokens

CodeBricks breaks Decrypt String into its right-to-left scanning and character mapping building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the parsing pattern is automatic. When a string decoding problem shows up in your interview, you do not think about it. You just write it.