Facebook Pixel

3121. Count the Number of Special Characters II

MediumHash TableString
Leetcode Link

Problem Description

You are given a string word that contains letters. A letter c is considered special if it meets two conditions:

  1. The letter appears in both lowercase and uppercase forms in the string (e.g., both 'a' and 'A' are present)
  2. Every lowercase occurrence of that letter appears before the first uppercase occurrence of the same letter

Your task is to count how many special letters exist in the given string.

For example, if we have a string where 'a' appears at positions 1, 3, 5 and 'A' appears at positions 7, 9, then 'a'/'A' would be special because all lowercase 'a's (positions 1, 3, 5) come before the first uppercase 'A' (position 7).

The solution uses two hash tables to track the first and last occurrences of each character. By comparing the last occurrence of a lowercase letter with the first occurrence of its uppercase counterpart, we can determine if that letter is special. If last[lowercase] < first[uppercase], then all lowercase occurrences must come before the first uppercase occurrence, making it a special letter.

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

Intuition

To determine if a letter is special, we need to verify that all lowercase occurrences come before any uppercase occurrence. The key insight is that we don't need to track every single occurrence - we only need to know two critical pieces of information:

  1. The last position where the lowercase version appears
  2. The first position where the uppercase version appears

Why? Because if the last lowercase occurrence is before the first uppercase occurrence, then we can guarantee that ALL lowercase occurrences are before ANY uppercase occurrence.

Think of it this way: imagine we have lowercase 'a' at positions [2, 5, 8] and uppercase 'A' at positions [10, 15]. Instead of checking if 2 < 10, 5 < 10, and 8 < 10, we can simply check if 8 (the last lowercase) < 10 (the first uppercase). If this condition holds true, then all other lowercase positions must also be less than 10.

This observation leads us to use two hash tables:

  • first: stores where each character appears for the first time
  • last: stores where each character appears for the last time

As we iterate through the string once, we update these positions. Then, for each letter pair (like 'a' and 'A'), we check if last['a'] < first['A']. If both versions exist and this condition is satisfied, we've found a special letter.

This approach is efficient because we reduce the problem from tracking all occurrences to just tracking the boundary positions that matter for our comparison.

Solution Approach

The implementation uses hash tables to track character positions and follows these steps:

  1. Initialize two hash tables: Create first and last dictionaries to store the first and last occurrence positions of each character in the string.

  2. Single pass through the string: Iterate through word with index i and character c:

    • If character c hasn't been seen before (c not in first), record its position in first[c] = i
    • Always update last[c] = i to track the most recent position of character c
  3. Count special letters: Use Python's zip function to pair each lowercase letter with its uppercase counterpart from ascii_lowercase and ascii_uppercase. For each pair (a, b) where a is lowercase and b is uppercase:

    • Check if lowercase a exists in the string: a in last
    • Check if uppercase b exists in the string: b in first
    • Check if all lowercase occurrences come before the first uppercase: last[a] < first[b]
    • If all three conditions are true, this letter is special
  4. Return the count: The sum() function counts how many letter pairs satisfy all conditions, giving us the total number of special letters.

The time complexity is O(n + 26) where n is the length of the string (for the initial traversal) and 26 is for checking all letter pairs. The space complexity is O(1) since we store at most 52 character positions (26 lowercase + 26 uppercase).

The elegance of this solution lies in reducing the problem to boundary checking - we only need the last lowercase and first uppercase positions to determine if a letter is special.

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 solution with the string word = "aAbBcC".

Step 1: Build the position hash tables

We iterate through the string and track first and last occurrences:

IndexCharacterActionfirstlast
0'a'First occurrence of 'a'{'a': 0}{'a': 0}
1'A'First occurrence of 'A'{'a': 0, 'A': 1}{'a': 0, 'A': 1}
2'b'First occurrence of 'b'{'a': 0, 'A': 1, 'b': 2}{'a': 0, 'A': 1, 'b': 2}
3'B'First occurrence of 'B'{'a': 0, 'A': 1, 'b': 2, 'B': 3}{'a': 0, 'A': 1, 'b': 2, 'B': 3}
4'c'First occurrence of 'c'{'a': 0, 'A': 1, 'b': 2, 'B': 3, 'c': 4}{'a': 0, 'A': 1, 'b': 2, 'B': 3, 'c': 4}
5'C'First occurrence of 'C'{'a': 0, 'A': 1, 'b': 2, 'B': 3, 'c': 4, 'C': 5}{'a': 0, 'A': 1, 'b': 2, 'B': 3, 'c': 4, 'C': 5}

Step 2: Check each letter pair for special condition

Now we check each lowercase/uppercase pair:

Letter PairLowercase in string?Uppercase in string?last[lowercase] < first[uppercase]?Special?
('a', 'A')✓ (a exists)✓ (A exists)0 < 1 ✓Yes
('b', 'B')✓ (b exists)✓ (B exists)2 < 3 ✓Yes
('c', 'C')✓ (c exists)✓ (C exists)4 < 5 ✓Yes
('d', 'D')✗ (d doesn't exist)--No
...............

Step 3: Count special letters

We found 3 special letters: a/A, b/B, and c/C.

Each satisfies the condition that all lowercase occurrences appear before the first uppercase occurrence.

Another example to illustrate the boundary checking:

Consider word = "aabAAa":

  • 'a' appears at positions 0, 1, 5
  • 'A' appears at positions 3, 4

After building our hash tables:

  • first['a'] = 0, last['a'] = 5
  • first['A'] = 3, last['A'] = 4

When checking if 'a'/'A' is special:

  • Both 'a' and 'A' exist ✓
  • last['a'] < first['A']? → 5 < 3? → False ✗

The letter 'a'/'A' is NOT special because the lowercase 'a' at position 5 comes after the uppercase 'A' at position 3. This demonstrates why tracking just the last lowercase and first uppercase positions is sufficient - if the last lowercase violates the condition, then the letter cannot be special.

Solution Implementation

1from string import ascii_lowercase, ascii_uppercase
2
3class Solution:
4    def numberOfSpecialChars(self, word: str) -> int:
5        # Dictionary to store the first occurrence index of each character
6        first_occurrence = {}
7        # Dictionary to store the last occurrence index of each character
8        last_occurrence = {}
9      
10        # Iterate through the word to record first and last occurrences
11        for index, char in enumerate(word):
12            # Record first occurrence if not already recorded
13            if char not in first_occurrence:
14                first_occurrence[char] = index
15            # Always update last occurrence
16            last_occurrence[char] = index
17      
18        # Count special characters where lowercase appears before uppercase
19        # A character is special if:
20        # 1. Its lowercase version exists (has a last occurrence)
21        # 2. Its uppercase version exists (has a first occurrence)
22        # 3. All lowercase occurrences come before all uppercase occurrences
23        special_char_count = sum(
24            lowercase_char in last_occurrence 
25            and uppercase_char in first_occurrence 
26            and last_occurrence[lowercase_char] < first_occurrence[uppercase_char]
27            for lowercase_char, uppercase_char in zip(ascii_lowercase, ascii_uppercase)
28        )
29      
30        return special_char_count
31
1class Solution {
2    public int numberOfSpecialChars(String word) {
3        // Arrays to track first and last occurrence positions of each character
4        // Size is 'z' + 1 to accommodate all ASCII values from 'A' to 'z'
5        int[] firstOccurrence = new int['z' + 1];
6        int[] lastOccurrence = new int['z' + 1];
7      
8        // Iterate through the word and record positions (1-indexed)
9        for (int i = 1; i <= word.length(); i++) {
10            char currentChar = word.charAt(i - 1);
11          
12            // Record first occurrence if not yet recorded
13            if (firstOccurrence[currentChar] == 0) {
14                firstOccurrence[currentChar] = i;
15            }
16          
17            // Always update last occurrence
18            lastOccurrence[currentChar] = i;
19        }
20      
21        // Count special characters
22        int specialCharCount = 0;
23      
24        // Check each letter of the alphabet
25        for (int i = 0; i < 26; i++) {
26            char lowercaseLetter = (char) ('a' + i);
27            char uppercaseLetter = (char) ('A' + i);
28          
29            // A character is special if:
30            // 1. Its lowercase version exists (lastOccurrence > 0)
31            // 2. Its uppercase version exists (firstOccurrence > 0)
32            // 3. All lowercase occurrences come before all uppercase occurrences
33            if (lastOccurrence[lowercaseLetter] > 0 && 
34                firstOccurrence[uppercaseLetter] > 0 && 
35                lastOccurrence[lowercaseLetter] < firstOccurrence[uppercaseLetter]) {
36                specialCharCount++;
37            }
38        }
39      
40        return specialCharCount;
41    }
42}
43
1class Solution {
2public:
3    int numberOfSpecialChars(string word) {
4        // Arrays to track first and last occurrence positions of each character
5        // Size is 'z' + 1 to accommodate all ASCII values from 'A' to 'z'
6        vector<int> firstOccurrence('z' + 1, 0);
7        vector<int> lastOccurrence('z' + 1, 0);
8      
9        // Iterate through the word and record positions (1-indexed)
10        for (int i = 0; i < word.size(); ++i) {
11            char currentChar = word[i];
12            int position = i + 1;  // Convert to 1-indexed position
13          
14            // Record first occurrence if not yet recorded
15            if (firstOccurrence[currentChar] == 0) {
16                firstOccurrence[currentChar] = position;
17            }
18            // Always update last occurrence
19            lastOccurrence[currentChar] = position;
20        }
21      
22        // Count special characters
23        int specialCharCount = 0;
24      
25        // Check each letter (a-z / A-Z pairs)
26        for (int i = 0; i < 26; ++i) {
27            char lowerCase = 'a' + i;
28            char upperCase = 'A' + i;
29          
30            // A character is special if:
31            // 1. Both lowercase and uppercase versions exist
32            // 2. All lowercase occurrences come before all uppercase occurrences
33            if (lastOccurrence[lowerCase] > 0 && 
34                firstOccurrence[upperCase] > 0 && 
35                lastOccurrence[lowerCase] < firstOccurrence[upperCase]) {
36                ++specialCharCount;
37            }
38        }
39      
40        return specialCharCount;
41    }
42};
43
1/**
2 * Counts the number of special characters in a word.
3 * A character is special if its lowercase version appears before its uppercase version.
4 * 
5 * @param word - The input string to analyze
6 * @returns The count of special characters
7 */
8function numberOfSpecialChars(word: string): number {
9    // ASCII value of 'z' is 122, creating arrays of size 123 to cover all lowercase/uppercase letters
10    const ASCII_SIZE = 'z'.charCodeAt(0) + 1;
11  
12    // Track the first occurrence position (1-indexed) of each character
13    const firstOccurrence: number[] = Array.from({ length: ASCII_SIZE }, () => 0);
14  
15    // Track the last occurrence position (1-indexed) of each character
16    const lastOccurrence: number[] = Array.from({ length: ASCII_SIZE }, () => 0);
17  
18    // Iterate through the word to record first and last occurrences
19    for (let i = 0; i < word.length; i++) {
20        const charCode = word.charCodeAt(i);
21      
22        // Set first occurrence only if not previously set (using 1-indexed position)
23        if (firstOccurrence[charCode] === 0) {
24            firstOccurrence[charCode] = i + 1;
25        }
26      
27        // Always update last occurrence (using 1-indexed position)
28        lastOccurrence[charCode] = i + 1;
29    }
30  
31    let specialCharCount = 0;
32    const LOWERCASE_A = 'a'.charCodeAt(0);
33    const UPPERCASE_A = 'A'.charCodeAt(0);
34  
35    // Check each letter of the alphabet (26 letters)
36    for (let i = 0; i < 26; i++) {
37        const lowercaseCode = LOWERCASE_A + i;
38        const uppercaseCode = UPPERCASE_A + i;
39      
40        // A letter is special if:
41        // 1. Its lowercase version exists (lastOccurrence > 0)
42        // 2. Its uppercase version exists (firstOccurrence > 0)
43        // 3. The last lowercase occurrence comes before the first uppercase occurrence
44        if (lastOccurrence[lowercaseCode] > 0 &&
45            firstOccurrence[uppercaseCode] > 0 &&
46            lastOccurrence[lowercaseCode] < firstOccurrence[uppercaseCode]) {
47            specialCharCount++;
48        }
49    }
50  
51    return specialCharCount;
52}
53

Time and Space Complexity

Time Complexity: O(n + |Σ|)

The algorithm consists of two main parts:

  1. A single pass through the string word to build the first and last dictionaries, which takes O(n) time where n is the length of the string.
  2. A loop that iterates through the zip of ascii_lowercase and ascii_uppercase, which contains 26 pairs, taking O(26) = O(|Σ|) time where |Σ| represents the size of the character set being checked.

Therefore, the total time complexity is O(n + |Σ|), where in this problem |Σ| = 26 for the English alphabet pairs being checked.

Space Complexity: O(|Σ|)

The space is used by:

  1. The first dictionary which stores at most one entry per unique character in the string
  2. The last dictionary which stores at most one entry per unique character in the string

Since the string can contain both uppercase and lowercase letters, and we're dealing with ASCII characters, the maximum number of unique characters stored across both dictionaries is bounded by the character set size. In this problem, |Σ| ≤ 52 (26 lowercase + 26 uppercase letters), though the reference mentions |Σ| ≤ 128 considering the full ASCII character set that could appear in the string.

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

Common Pitfalls

1. Misunderstanding the "before" requirement

A common mistake is checking if any lowercase occurrence comes before any uppercase occurrence, rather than ensuring all lowercase occurrences come before the first uppercase occurrence.

Incorrect approach:

# Wrong: Only checks if first lowercase comes before first uppercase
if first_occurrence[lowercase] < first_occurrence[uppercase]:
    count += 1

Why it fails: Consider the string "aAa". The first 'a' (position 0) comes before 'A' (position 1), but the last 'a' (position 2) comes after 'A', so this letter should NOT be special.

Correct approach:

# Correct: Checks if last lowercase comes before first uppercase
if last_occurrence[lowercase] < first_occurrence[uppercase]:
    count += 1

2. Not handling missing characters properly

Another pitfall is not checking whether both forms (lowercase and uppercase) of a letter actually exist in the string before comparing their positions.

Incorrect approach:

# Wrong: May cause KeyError if character doesn't exist
for lower, upper in zip(ascii_lowercase, ascii_uppercase):
    if last_occurrence[lower] < first_occurrence[upper]:
        count += 1

Correct approach:

# Correct: Checks existence before accessing
for lower, upper in zip(ascii_lowercase, ascii_uppercase):
    if lower in last_occurrence and upper in first_occurrence:
        if last_occurrence[lower] < first_occurrence[upper]:
            count += 1

3. Using wrong comparison for boundary positions

Some might incorrectly use <= instead of < when comparing positions, which would incorrectly count cases where the last lowercase and first uppercase are at the same position (which is impossible for different characters).

Key insight: The solution elegantly reduces the problem to boundary checking - if the rightmost lowercase position is still to the left of the leftmost uppercase position, then ALL lowercase occurrences must be before ALL uppercase occurrences.

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

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More