387. First Unique Character in a String
Problem Description
You are given a string s
. Your task is to find the first character in the string that appears only once (non-repeating) and return its index position. If every character in the string appears more than once, return -1
.
For example:
- In the string
"leetcode"
, the character'l'
appears at index 0 and is the first non-repeating character, so return0
- In the string
"loveleetcode"
, the character'l'
appears multiple times,'o'
appears multiple times, but'v'
at index 2 is the first character that appears only once, so return2
- In the string
"aabb"
, all characters repeat, so return-1
The solution approach counts the frequency of each character in the string using a Counter (hash table). Then it iterates through the string from the beginning, checking each character's count. The first character with a count of exactly 1
is the first non-repeating character, and its index is returned. If no such character exists after checking all positions, -1
is returned.
Intuition
To find the first non-repeating character, we need to know which characters appear only once in the entire string. The key insight is that we must examine the entire string first to determine the frequency of each character before we can identify which one is non-repeating.
Why can't we solve this in a single pass? Consider the string "aab"
. When we encounter the first 'a'
at index 0, we might think it's unique, but we later discover another 'a'
at index 1. This shows we need complete frequency information before making any decisions.
The natural approach is to break this into two phases:
- Count all characters: First pass through the string to build a frequency map. This tells us exactly how many times each character appears.
- Find the first unique character: Second pass through the string in order, checking each character's frequency. The first character with frequency
1
is our answer.
Why do we need the second pass? Because we want the first non-repeating character by position, not just any non-repeating character. The frequency map doesn't preserve the order of characters in the original string. By iterating through the string again from the beginning and checking against our frequency map, we ensure we return the earliest position where a unique character occurs.
This two-pass approach with O(n)
time complexity is optimal because we must examine every character at least once to know their frequencies, and we need to maintain the original order to find the "first" occurrence.
Solution Approach
The implementation uses a hash table to count character frequencies, then performs a linear scan to find the first unique character.
Step 1: Count Character Frequencies
We use Python's Counter
from the collections module to create a frequency map:
cnt = Counter(s)
This single line traverses the entire string s
and builds a hash table where:
- Keys are the characters in the string
- Values are the number of times each character appears
For example, if s = "leetcode"
, then cnt
would be:
{'l': 1, 'e': 3, 't': 1, 'c': 1, 'o': 1, 'd': 1}
Step 2: Find First Unique Character
We iterate through the string again using enumerate()
to track both the index and character:
for i, c in enumerate(s):
if cnt[c] == 1:
return i
During this iteration:
i
represents the current index positionc
represents the character at positioni
- We check if
cnt[c] == 1
, meaning this character appears exactly once - If true, we immediately return the index
i
since this is the first unique character
Step 3: Handle No Unique Characters
If the loop completes without finding any character with frequency 1:
return -1
Time and Space Complexity:
- Time Complexity:
O(n)
wheren
is the length of the string. We traverse the string twice - once to build the frequency map and once to find the first unique character. - Space Complexity:
O(1)
orO(k)
wherek
is the number of unique characters. Since we're dealing with lowercase English letters, the space is bounded by 26, making it effectively constant.
The solution is optimal because we must examine every character at least once to determine uniqueness, and the two-pass approach maintains the simplicity while preserving the order requirement.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the string s = "loveleetcode"
.
Step 1: Count Character Frequencies
First, we build a frequency map by counting each character:
l
appears at positions 0, 4, 5 → count = 3o
appears at positions 1, 7 → count = 2v
appears at position 2 → count = 1e
appears at positions 3, 6, 8, 11 → count = 4t
appears at position 9 → count = 1c
appears at position 10 → count = 1d
appears at position 12 → count = 1
Our frequency map: {'l': 3, 'o': 2, 'v': 1, 'e': 4, 't': 1, 'c': 1, 'd': 1}
Step 2: Find First Unique Character
Now we iterate through the string from the beginning, checking each character's count:
- Index 0:
s[0] = 'l'
,cnt['l'] = 3
→ Not unique (count > 1), continue - Index 1:
s[1] = 'o'
,cnt['o'] = 2
→ Not unique (count > 1), continue - Index 2:
s[2] = 'v'
,cnt['v'] = 1
→ Unique! (count = 1), return 2
We found our answer at index 2. The character 'v'
is the first character that appears only once in the string.
Why Two Passes?
Notice that we have other unique characters ('t'
, 'c'
, 'd'
) but they appear later in the string. Without the first pass to count all frequencies, we wouldn't know that 'l'
at index 0 actually repeats later. The second pass ensures we return the first unique character by position, which is 'v'
at index 2, not just any unique character.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def firstUniqChar(self, s: str) -> int:
5 """
6 Find the first non-repeating character in a string and return its index.
7 If it doesn't exist, return -1.
8
9 Args:
10 s: Input string to search for first unique character
11
12 Returns:
13 Index of first unique character, or -1 if none exists
14 """
15 # Count frequency of each character in the string
16 char_count = Counter(s)
17
18 # Iterate through string to find first character with count of 1
19 for index, char in enumerate(s):
20 if char_count[char] == 1:
21 return index
22
23 # No unique character found
24 return -1
25
1class Solution {
2 /**
3 * Finds the index of the first non-repeating character in a string.
4 *
5 * @param s The input string containing only lowercase English letters
6 * @return The index of the first unique character, or -1 if none exists
7 */
8 public int firstUniqChar(String s) {
9 // Array to store frequency count for each lowercase letter (a-z)
10 int[] frequencyCount = new int[26];
11 int stringLength = s.length();
12
13 // First pass: Count the frequency of each character
14 for (int i = 0; i < stringLength; i++) {
15 // Map character to array index (0-25) by subtracting 'a'
16 int charIndex = s.charAt(i) - 'a';
17 frequencyCount[charIndex]++;
18 }
19
20 // Second pass: Find the first character with frequency of 1
21 for (int i = 0; i < stringLength; i++) {
22 int charIndex = s.charAt(i) - 'a';
23 // Check if current character appears exactly once
24 if (frequencyCount[charIndex] == 1) {
25 return i;
26 }
27 }
28
29 // No unique character found
30 return -1;
31 }
32}
33
1class Solution {
2public:
3 int firstUniqChar(string s) {
4 // Array to store frequency count of each character (26 lowercase letters)
5 int charFrequency[26] = {0};
6
7 // First pass: Count frequency of each character in the string
8 for (char& c : s) {
9 charFrequency[c - 'a']++;
10 }
11
12 // Store string length for iteration
13 int stringLength = s.size();
14
15 // Second pass: Find the first character with frequency of 1
16 for (int i = 0; i < stringLength; ++i) {
17 if (charFrequency[s[i] - 'a'] == 1) {
18 return i; // Return the index of first unique character
19 }
20 }
21
22 // No unique character found
23 return -1;
24 }
25};
26
1/**
2 * Finds the index of the first non-repeating character in a string
3 * @param s - The input string to search
4 * @returns The index of the first unique character, or -1 if none exists
5 */
6function firstUniqChar(s: string): number {
7 // Create a frequency map to count occurrences of each character
8 const characterFrequency: Map<string, number> = new Map<string, number>();
9
10 // First pass: Count the frequency of each character
11 for (const character of s) {
12 const currentCount: number = characterFrequency.get(character) || 0;
13 characterFrequency.set(character, currentCount + 1);
14 }
15
16 // Second pass: Find the first character with frequency of 1
17 for (let index: number = 0; index < s.length; index++) {
18 const currentCharacter: string = s[index];
19
20 // Check if current character appears only once
21 if (characterFrequency.get(currentCharacter) === 1) {
22 return index;
23 }
24 }
25
26 // No unique character found
27 return -1;
28}
29
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. This is because:
- Creating the Counter takes
O(n)
time as it needs to iterate through all characters in the string once - The enumerate loop iterates through the string once more, which is another
O(n)
operation - The lookup operation
cnt[c]
inside the loop isO(1)
for a hash table - Overall:
O(n) + O(n) = O(n)
The space complexity is O(|Σ|)
, where Σ
is the character set. Since the problem deals with lowercase English letters, |Σ| = 26
. The Counter (hash table) will store at most 26 key-value pairs, one for each unique lowercase letter that appears in the string. This gives us a constant space complexity of O(26) = O(1)
when considering the character set is fixed, or more generally O(|Σ|)
when considering variable character sets.
Common Pitfalls
Pitfall 1: Attempting to Use the Counter Keys Directly
A common mistake is trying to iterate through the Counter's keys instead of the original string, which loses the original order:
Incorrect Approach:
def firstUniqChar(self, s: str) -> int:
char_count = Counter(s)
# Wrong: iterating through counter keys doesn't preserve original order
for char in char_count:
if char_count[char] == 1:
return s.index(char) # This might not return the FIRST unique character
return -1
Why it's wrong: The Counter's keys may not maintain the order of first appearance. For example, with s = "loveleetcode"
, iterating through char_count.keys()
might process characters in an arbitrary order, potentially returning the index of 'd' (10) instead of 'v' (2).
Solution: Always iterate through the original string to maintain the order of characters as they appear.
Pitfall 2: Using str.index()
After Finding a Unique Character
Incorrect Approach:
def firstUniqChar(self, s: str) -> int:
char_count = Counter(s)
for char in s:
if char_count[char] == 1:
return s.index(char) # Inefficient and redundant
return -1
Why it's problematic: While this works correctly, using s.index(char)
adds unnecessary overhead. The index()
method performs another O(n) search in the worst case, even though we already know the position from our iteration.
Solution: Use enumerate()
to track indices during iteration, avoiding the need for an additional search.
Pitfall 3: Modifying the Counter During Iteration
Incorrect Approach:
def firstUniqChar(self, s: str) -> int:
char_count = Counter(s)
for i, char in enumerate(s):
if char_count[char] == 1:
return i
else:
# Wrong: modifying the counter during iteration
del char_count[char]
return -1
Why it's wrong: Deleting entries from the Counter while iterating can lead to incorrect results for subsequent characters. If a character appears multiple times, deleting it after the first occurrence would make later occurrences appear to be unique.
Solution: Never modify the frequency map during the search phase. Keep it immutable after the initial count.
Pitfall 4: Not Handling Edge Cases
Incorrect Approach:
def firstUniqChar(self, s: str) -> int:
# Missing edge case handling
char_count = Counter(s)
for i, char in enumerate(s):
if char_count[char] == 1:
return i
Why it's incomplete: The code doesn't explicitly handle the case where no unique character exists, potentially causing unexpected behavior or errors in some implementations.
Solution: Always include an explicit return statement after the loop to handle the case where no unique character is found:
for i, char in enumerate(s):
if char_count[char] == 1:
return i
return -1 # Explicitly handle the no-unique-character case
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
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
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
Want a Structured Path to Master System Design Too? Don’t Miss This!