3121. Count the Number of Special Characters II
Problem Description
You are given a string word
that contains letters. A letter c
is considered special if it meets two conditions:
- The letter appears in both lowercase and uppercase forms in the string (e.g., both 'a' and 'A' are present)
- 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.
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:
- The last position where the lowercase version appears
- 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 timelast
: 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:
-
Initialize two hash tables: Create
first
andlast
dictionaries to store the first and last occurrence positions of each character in the string. -
Single pass through the string: Iterate through
word
with indexi
and characterc
:- If character
c
hasn't been seen before (c not in first
), record its position infirst[c] = i
- Always update
last[c] = i
to track the most recent position of characterc
- If character
-
Count special letters: Use Python's
zip
function to pair each lowercase letter with its uppercase counterpart fromascii_lowercase
andascii_uppercase
. For each pair(a, b)
wherea
is lowercase andb
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
- Check if lowercase
-
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 EvaluatorExample 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:
Index | Character | Action | first | last |
---|---|---|---|---|
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 Pair | Lowercase 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:
- A single pass through the string
word
to build thefirst
andlast
dictionaries, which takesO(n)
time wheren
is the length of the string. - A loop that iterates through the zip of
ascii_lowercase
andascii_uppercase
, which contains 26 pairs, takingO(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:
- The
first
dictionary which stores at most one entry per unique character in the string - 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.
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!