014. First Unique Character
Problem
Return the smallest zero-based index whose character occurs exactly once in the entire string. Return -1 if there is no such character. Comparisons are case-sensitive, and the input is unchanged.
Examples
Example 1
first_unique("leetcode")
=> 0The initial l is globally unique.
Example 2
first_unique("aabb")
=> -1Every character repeats.
Example 3
first_unique("")
=> -1There is no index to return.
Reveal the worked solution and reasoning
Why this mechanism works
A character that looks unique now may recur later. Finish counting before committing to the earliest index. The second pass retains input order, while the dictionary answers the global frequency question.
State and boundaries
counts is complete before selection. i is the next candidate index in original order.
Protocol
- Count all characters.
- Scan indices from the beginning.
- Return the first index with count one.
- Return -1 if none qualifies.
Visualization
leetcode
counts: l=1, e=3, t=1, c=1, o=1, d=1
scan from left -> index 0Python implementation
def first_unique(text):
counts = {}
for char in text:
counts[char] = counts.get(char, 0) + 1
for i, char in enumerate(text):
if counts[char] == 1:
return i
return -1
Worked execution
first_unique("leetcode")
L2 first_unique: counts = {}
text='leetcode'; counts={}
L3 first_unique: for char in text:
text='leetcode'; counts={}; char='l'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='leetcode'; counts={'l':1}; char='l'
L3 first_unique: for char in text:
text='leetcode'; counts={'l':1}; char='e'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='leetcode'; counts={'l':1, 'e':1}; char='e'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='leetcode'; counts={'l':1, 'e':2}; char='e'
L3 first_unique: for char in text:
text='leetcode'; counts={'l':1, 'e':2}; char='t'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='leetcode'; counts={'l':1, 'e':2, 't':1}; char='t'
... 5 intermediate state changes omitted ...
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='leetcode'; counts={'l':1, 'e':2, 't':1, 'c':1, 'o':1, ...}; char='d'
L3 first_unique: for char in text:
text='leetcode'; counts={'l':1, 'e':2, 't':1, 'c':1, 'o':1, ...}; char='e'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='leetcode'; counts={'l':1, 'e':3, 't':1, 'c':1, 'o':1, ...}; char='e'
L5 first_unique: for i, char in enumerate(text):
text='leetcode'; counts={'l':1, 'e':3, 't':1, 'c':1, 'o':1, ...}; char='l'; i=0Result: 0. The initial l is globally unique.
first_unique("aabb")
L2 first_unique: counts = {}
text='aabb'; counts={}
L3 first_unique: for char in text:
text='aabb'; counts={}; char='a'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='aabb'; counts={'a':1}; char='a'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='aabb'; counts={'a':2}; char='a'
L3 first_unique: for char in text:
text='aabb'; counts={'a':2}; char='b'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='aabb'; counts={'a':2, 'b':1}; char='b'
L4 first_unique: counts[char] = counts.get(char, 0) + 1
text='aabb'; counts={'a':2, 'b':2}; char='b'
L5 first_unique: for i, char in enumerate(text):
text='aabb'; counts={'a':2, 'b':2}; char='a'; i=0
L5 first_unique: for i, char in enumerate(text):
text='aabb'; counts={'a':2, 'b':2}; char='a'; i=1
L5 first_unique: for i, char in enumerate(text):
text='aabb'; counts={'a':2, 'b':2}; char='b'; i=2
L5 first_unique: for i, char in enumerate(text):
text='aabb'; counts={'a':2, 'b':2}; char='b'; i=3Result: -1. Every character repeats.
Why the solution is correct
The counting pass determines final multiplicities. The selection pass tests candidates in increasing index order, so its first qualifying index is both globally unique and earliest. Exhaustion means every character is repeated.
Time and space
O(n) expected time and O(u) auxiliary space.
Common mistakes
Returning during the counting pass commits before future occurrences are known. Returning a dictionary key does not provide the requested index.
Reasoning card
State: counts is complete before selection. i is the next candidate index in original order.
Invariant: The frequency map is complete before choosing the answer.
Decision to rehearse: Separate the counting pass from the ordered selection pass.
Finish: Return -1 if none qualifies
Transfer exercise
Maintain the first unique character after every stream insertion; identify the extra order state a streaming solution needs.