Facebook Pixel

1309. Decrypt String from Alphabet to Integer Mapping

EasyString
Leetcode Link

Problem Description

You are given a string s that contains only digits ('1' to '9') and the character '#'. Your task is to decode this string into English lowercase letters using the following mapping rules:

  • Single digits '1' through '9' map to letters 'a' through 'i' respectively

    • '1''a'
    • '2''b'
    • '3''c'
    • ... and so on until '9''i'
  • Two-digit numbers followed by '#' (from '10#' to '26#') map to letters 'j' through 'z' respectively

    • '10#''j'
    • '11#''k'
    • '12#''l'
    • ... and so on until '26#''z'

The solution traverses the string from left to right. At each position, it checks if there's a '#' character two positions ahead. If there is, it treats the current and next digit as a two-digit number (representing letters 'j' to 'z'), processes them together, and moves forward by 3 positions. If there's no '#' ahead, it treats the current digit as a single-digit number (representing letters 'a' to 'i') and moves forward by 1 position.

For example:

  • Input: "1326#" would be decoded as "acz" (where '1''a', '3''c', and '26#''z')
  • Input: "25#" would be decoded as "y" (where '25#''y')

The problem guarantees that the input will always have a valid unique mapping, meaning the string can be unambiguously decoded into letters.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that we need to parse the string from left to right, but we must look ahead to determine how to interpret each digit. The presence of '#' acts as a delimiter that changes the meaning of the preceding digits.

When we encounter digits in the string, we face a decision: should we treat them as a single-digit mapping ('1' to '9''a' to 'i') or as part of a two-digit mapping ('10#' to '26#''j' to 'z')? The '#' symbol is the critical indicator - it only appears after two-digit numbers.

This leads us to a greedy approach: at each position, we first check if there's a '#' two positions ahead. If there is, we know the current and next digits form a two-digit number, so we process them together. If not, the current digit stands alone.

Why does this greedy strategy work? Because the problem guarantees a unique valid mapping. There's no ambiguity - if we see '#' at position i+2, then positions i and i+1 must form a two-digit number. We can't have a situation where we need to "backtrack" and reconsider our decisions.

The conversion itself is straightforward once we know which digits to group together. For a number n representing a letter:

  • We calculate the character as chr(n + ord('a') - 1)
  • The -1 offset is because 'a' corresponds to 1 (not 0), 'b' to 2, and so on

This approach processes each character exactly once, making it efficient and clean.

Solution Approach

The implementation uses a simple simulation approach with a single pass through the string. Here's how the algorithm works step by step:

Initialize Variables:

  • Create an empty list ans to store the decoded characters
  • Set two pointers: i = 0 (current position) and n = len(s) (string length)

Main Loop - Traverse the String:

While i < n, we perform the following checks at each position:

  1. Check for Two-Digit Pattern:

    • First verify if i + 2 < n (ensuring we don't go out of bounds)
    • Then check if s[i + 2] == '#'
    • If both conditions are true, we have a two-digit number followed by '#'
  2. Process Two-Digit Number (letters j-z):

    • Extract the substring s[i : i + 2] to get the two-digit number
    • Convert it to an integer using int(s[i : i + 2])
    • Calculate the corresponding character: chr(int(s[i : i + 2]) + ord('a') - 1)
    • Append the character to ans
    • Move forward by 3 positions: i += 3 (skip the two digits and the '#')
  3. Process Single-Digit Number (letters a-i):

    • If there's no '#' at position i + 2, treat the current digit as standalone
    • Convert s[i] to an integer
    • Calculate the corresponding character: chr(int(s[i]) + ord('a') - 1)
    • Append the character to ans
    • Move forward by 1 position: i += 1

Character Conversion Formula:

The formula chr(num + ord('a') - 1) works because:

  • ord('a') gives us the ASCII value of 'a'
  • We subtract 1 because the mapping starts at 1 (not 0): '1' maps to 'a', '2' to 'b', etc.
  • chr() converts the resulting ASCII value back to a character

Final Step:

After processing all characters, join the list ans into a string using ''.join(ans) and return it.

Time and Space Complexity:

  • Time: O(n) where n is the length of the input string - we visit each character once
  • Space: O(n) for storing the result array

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the decoding process with the input string "12#321".

Initial Setup:

  • Input string: s = "12#321"
  • Empty result list: ans = []
  • Starting position: i = 0
  • String length: n = 6

Step 1: Position i = 0

  • Current character: '1'
  • Check if i + 2 < n: 0 + 2 < 6
  • Check if s[i + 2] == '#': s[2] = '#'
  • We found a two-digit pattern! Extract s[0:2] = "12"
  • Convert: int("12") = 12
  • Calculate character: chr(12 + ord('a') - 1) = chr(12 + 97 - 1) = chr(108) = 'l'
  • Append 'l' to result: ans = ['l']
  • Move forward by 3: i = 3

Step 2: Position i = 3

  • Current character: '3'
  • Check if i + 2 < n: 3 + 2 < 6
  • Check if s[i + 2] == '#': s[5] = '1'
  • No '#' found, treat as single digit
  • Convert: int('3') = 3
  • Calculate character: chr(3 + ord('a') - 1) = chr(3 + 97 - 1) = chr(99) = 'c'
  • Append 'c' to result: ans = ['l', 'c']
  • Move forward by 1: i = 4

Step 3: Position i = 4

  • Current character: '2'
  • Check if i + 2 < n: 4 + 2 < 6 ✗ (would be equal to 6, not less than)
  • Can't look ahead for '#', treat as single digit
  • Convert: int('2') = 2
  • Calculate character: chr(2 + ord('a') - 1) = chr(2 + 97 - 1) = chr(98) = 'b'
  • Append 'b' to result: ans = ['l', 'c', 'b']
  • Move forward by 1: i = 5

Step 4: Position i = 5

  • Current character: '1'
  • Check if i + 2 < n: 5 + 2 < 6
  • Can't look ahead for '#', treat as single digit
  • Convert: int('1') = 1
  • Calculate character: chr(1 + ord('a') - 1) = chr(1 + 97 - 1) = chr(97) = 'a'
  • Append 'a' to result: ans = ['l', 'c', 'b', 'a']
  • Move forward by 1: i = 6

Final Step:

  • i = 6 is not less than n = 6, so exit the loop
  • Join the result: ''.join(['l', 'c', 'b', 'a']) = "lcba"
  • Return: "lcba"

The string "12#321" successfully decodes to "lcba" where:

  • "12#"'l' (12th letter)
  • "3"'c' (3rd letter)
  • "2"'b' (2nd letter)
  • "1"'a' (1st letter)

Solution Implementation

1class Solution:
2    def freqAlphabets(self, s: str) -> str:
3        """
4        Decodes a string where 'a'-'i' are represented as '1'-'9',
5        and 'j'-'z' are represented as '10#'-'26#'.
6      
7        Args:
8            s: Encoded string containing digits and '#' symbols
9          
10        Returns:
11            Decoded string with alphabetic characters
12        """
13        result = []
14        index = 0
15        string_length = len(s)
16      
17        # Traverse the string from left to right
18        while index < string_length:
19            # Check if current position forms a two-digit number with '#'
20            # (represents letters 'j' through 'z')
21            if index + 2 < string_length and s[index + 2] == '#':
22                # Extract two-digit number and convert to corresponding letter
23                two_digit_num = int(s[index:index + 2])
24                # Convert number to letter (10 -> 'j', 11 -> 'k', etc.)
25                letter = chr(two_digit_num + ord('a') - 1)
26                result.append(letter)
27                # Move index past the two digits and '#'
28                index += 3
29            else:
30                # Single digit number (represents letters 'a' through 'i')
31                single_digit_num = int(s[index])
32                # Convert number to letter (1 -> 'a', 2 -> 'b', etc.)
33                letter = chr(single_digit_num + ord('a') - 1)
34                result.append(letter)
35                # Move to next character
36                index += 1
37      
38        # Join all decoded letters into final string
39        return ''.join(result)
40
1class Solution {
2    public String freqAlphabets(String s) {
3        int index = 0;
4        int length = s.length();
5        StringBuilder result = new StringBuilder();
6      
7        // Iterate through the string
8        while (index < length) {
9            // Check if current position forms a two-digit number with '#' (10-26 mapped to 'j'-'z')
10            if (index + 2 < length && s.charAt(index + 2) == '#') {
11                // Extract two-digit number and convert to corresponding character
12                int number = Integer.parseInt(s.substring(index, index + 2));
13                char character = (char) ('a' + number - 1);
14                result.append(character);
15                // Move index by 3 positions (2 digits + 1 '#')
16                index += 3;
17            } else {
18                // Single digit number (1-9 mapped to 'a'-'i')
19                int digit = Integer.parseInt(s.substring(index, index + 1));
20                char character = (char) ('a' + digit - 1);
21                result.append(character);
22                // Move index by 1 position
23                index++;
24            }
25        }
26      
27        return result.toString();
28    }
29}
30
1class Solution {
2public:
3    string freqAlphabets(string s) {
4        string result = "";
5        int index = 0;
6        int stringLength = s.size();
7      
8        // Iterate through the string to decode the message
9        while (index < stringLength) {
10            // Check if current position forms a two-digit number followed by '#'
11            // Two-digit numbers (10-26) represent letters 'j' to 'z'
12            if (index + 2 < stringLength && s[index + 2] == '#') {
13                // Extract the two-digit number and convert to corresponding letter
14                // stoi converts string to integer, then add to 'a' - 1 to get the letter
15                // For example: "10" -> 10 -> 'a' + 10 - 1 = 'j'
16                int twoDigitNum = stoi(s.substr(index, 2));
17                result += char(twoDigitNum + 'a' - 1);
18                index += 3;  // Move past the two digits and '#'
19            } 
20            else {
21                // Single digit number (1-9) represents letters 'a' to 'i'
22                // Convert character digit to integer and then to corresponding letter
23                // For example: '1' -> 1 -> 'a' + 1 - 1 = 'a'
24                int singleDigitNum = s[index] - '0';
25                result += char(singleDigitNum + 'a' - 1);
26                index += 1;  // Move to next character
27            }
28        }
29      
30        return result;
31    }
32};
33
1/**
2 * Decodes a string where letters are encoded as numbers:
3 * - 'a' to 'i' are represented by '1' to '9'
4 * - 'j' to 'z' are represented by '10#' to '26#'
5 * 
6 * @param s - The encoded string to decode
7 * @returns The decoded string with letters
8 */
9function freqAlphabets(s: string): string {
10    const decodedCharacters: string[] = [];
11    const stringLength: number = s.length;
12  
13    let currentIndex: number = 0;
14  
15    // Iterate through the string to decode each character
16    while (currentIndex < stringLength) {
17        // Check if current position represents a two-digit number (10-26) followed by '#'
18        if (currentIndex + 2 < stringLength && s[currentIndex + 2] === '#') {
19            // Extract the two-digit number and convert to corresponding letter
20            const twoDigitNumber: number = parseInt(s.slice(currentIndex, currentIndex + 2));
21            const decodedChar: string = String.fromCharCode(96 + twoDigitNumber);
22            decodedCharacters.push(decodedChar);
23            currentIndex += 3; // Move past the two digits and '#'
24        } else {
25            // Single digit number (1-9), convert to corresponding letter
26            const singleDigitNumber: number = parseInt(s[currentIndex]);
27            const decodedChar: string = String.fromCharCode(96 + singleDigitNumber);
28            decodedCharacters.push(decodedChar);
29            currentIndex += 1; // Move to next character
30        }
31    }
32  
33    // Join all decoded characters into final string
34    return decodedCharacters.join('');
35}
36

Time and Space Complexity

Time Complexity: O(n)

The algorithm iterates through the string s exactly once using a while loop. In each iteration, the pointer i advances by either 1 or 3 positions. Regardless of the step size, each character in the string is visited at most once. The operations inside the loop (checking conditions, string slicing s[i:i+2], character conversion with chr() and ord(), and appending to the list) all take constant time O(1). Therefore, the overall time complexity is O(n), where n is the length of the string s.

Space Complexity: O(n)

The algorithm uses an additional list ans to store the decoded characters. In the worst case, when all mappings are single-digit (like "123456789"), the output will have n characters, requiring O(n) space. The final "".join(ans) operation creates a new string which also takes O(n) space. The other variables (i, n) use constant space O(1). Therefore, the overall space complexity is O(n), where n is the length of the string s.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Index Out of Bounds When Checking for '#'

One of the most common mistakes is incorrectly checking for the '#' character without proper boundary validation. Developers often write:

Incorrect Code:

if s[i + 2] == '#':  # Dangerous! Can cause IndexError
    # process two-digit number

The Problem: When i is at positions n-2 or n-1 (where n = len(s)), accessing s[i + 2] will raise an IndexError. This happens because we're trying to look ahead in the string without first verifying that those positions exist.

Correct Solution:

if i + 2 < len(s) and s[i + 2] == '#':  # Safe boundary check first
    # process two-digit number

Always check the boundary condition (i + 2 < len(s)) before attempting to access s[i + 2]. Python evaluates conditions from left to right and short-circuits, so if i + 2 >= len(s), it won't even attempt to access s[i + 2].

2. Incorrect Character Conversion Formula

Another frequent error involves the mathematical conversion from numbers to characters:

Incorrect Formulas:

# Wrong: Forgetting to subtract 1
letter = chr(int(s[i]) + ord('a'))  # '1' would map to 'b' instead of 'a'

# Wrong: Using wrong base character
letter = chr(int(s[i]) + ord('A') - 1)  # Would give uppercase letters

# Wrong: Direct ASCII addition without ord()
letter = chr(int(s[i]) + 97 - 1)  # Works but not readable/maintainable

The Problem:

  • The mapping starts at 1 (not 0), so '1' should map to 'a'. Without subtracting 1, '1' would incorrectly map to 'b'.
  • Using ord('A') would produce uppercase letters instead of lowercase.
  • Using magic numbers like 97 (ASCII value of 'a') makes code less readable.

Correct Solution:

letter = chr(int(num_str) + ord('a') - 1)

This formula correctly handles the 1-based indexing of the mapping.

3. Incorrect Index Advancement

Developers sometimes advance the index incorrectly after processing characters:

Incorrect Code:

if i + 2 < len(s) and s[i + 2] == '#':
    # process two-digit number
    i += 2  # Wrong! Only moves past the digits, not the '#'
else:
    # process single digit
    i += 1

The Problem: When processing a two-digit pattern like "10#", we need to skip all three characters (two digits plus the '#'). Moving only 2 positions would cause the '#' to be processed as if it were a digit in the next iteration.

Correct Solution:

if i + 2 < len(s) and s[i + 2] == '#':
    # process two-digit number
    i += 3  # Correctly skip both digits and the '#'
else:
    # process single digit
    i += 1

4. String Slicing Errors

When extracting the two-digit number, incorrect slicing is a common issue:

Incorrect Code:

two_digit_num = int(s[i] + s[i + 1])  # Works but less efficient
two_digit_num = int(s[i:i + 1])  # Wrong! Only gets one character
two_digit_num = int(s[i:i + 3])  # Wrong! Includes the '#'

The Problem:

  • String concatenation creates unnecessary temporary strings.
  • Incorrect slice boundaries either miss characters or include the '#'.

Correct Solution:

two_digit_num = int(s[i:i + 2])  # Cleanly extracts exactly two digits

Remember that Python slicing is exclusive of the end index, so s[i:i + 2] gets characters at positions i and i + 1.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More