Run it
The two-pass version, then the same answer read straight out of the ordered dictionary, then the streaming variant that answers after every single character.
Two passes
Pass one counts every character. Pass two walks the string in order and returns the first index whose count is 1.
O(n) time, O(k) space where k is the alphabet size — bounded by 26 for lowercase ASCII, so effectively O(1) for that case.
Why two passes are necessary: the answer depends on the total count of a character, which is not known until the whole string has been read. A single pass cannot decide whether the first character is unique before seeing the end.
Note the second loop iterates the string, not the counter. Iterating the counter would give the first character in insertion order that happens to have count 1, which is the same thing in CPython 3.7+ because dictionaries preserve insertion order — and relying on that is less clear than walking the string.
The single-pass variant
If asked for one pass over the input, the answer stores indices and resolves at the end:
Still O(n) overall, and the final scan is over the alphabet rather than the string — so for a long string with a small alphabet it touches less data.
Whether this counts as "one pass" is arguable, and the useful observation to make is that the information required is not available until the input ends, so some second look is unavoidable. Saying that is better than claiming a single-pass solution that does not exist.
Why one pass is impossible
Reading left to right, the first character might be unique or might repeat at the very end. Nothing can be returned until the whole string has been read, so a single pass that emits an answer as it goes is wrong by construction.
What a single pass can do is record enough to answer at the end — which is what the counter is. The second pass is not extra work in the complexity sense: two linear passes is still O(n).
Why the second pass walks the string
The answer must be the first such character, and "first" is a property of the string, not of the counter. Walking the string gets the order for free.
Since Python 3.7 dictionaries preserve insertion order, so walking counts.items() also works and touches only distinct characters. Say which guarantee you are relying on — it is a language guarantee since 3.7, and was an implementation detail in 3.6.
The stream variant
The real follow-up: "characters arrive one at a time and you must answer at any moment." Keep a counter and a queue of candidates. On each arrival, push it to the queue, then pop from the front while the front has a count above one. The head of the queue is always the current answer, in O(1) amortised.
The array optimisation
For a known small alphabet, an array replaces the dictionary:
Faster in practice — array indexing beats hashing — and it breaks on anything outside lowercase ASCII.
Worth offering as the optimised version with the assumption stated. Presenting it without noting that it assumes lowercase ASCII is the mistake; interviewers frequently follow up with "what about Unicode?"
| | Counter | Array |
|---|
| Time | O(n) | O(n) |
| Space | O(k) | O(26) |
| Alphabet | Any | Lowercase ASCII only |
| Constant factor | Higher | Lower |
Edge cases
| Case | Result | Why |
|---|
| Empty string | −1 | The loop does not run |
| All characters repeat | −1 | No count equals 1 |
| Single character | 0 | Count is 1 |
| All unique | 0 | The first character qualifies |
| Mixed case | "Aa" gives 0 — different characters | Ask whether case matters |
| Spaces and punctuation | Counted as characters | Ask whether they should be |
| Non-ASCII | Works with Counter, breaks with the array | Mention the assumption |
The mixed-case row is a legitimate clarifying question: is "Aa" two unique characters or one repeated one?
The variants
First repeating character. Simpler and genuinely one pass: keep a set of seen characters and return the first one already present.
That asymmetry is worth noticing: "first repeating" can be answered as soon as the repeat appears, so one pass suffices. "First non-repeating" cannot, because uniqueness is only knowable at the end.
First unique in a stream. A queue of candidates plus a count map: push each new character, and pop from the front while the front's count exceeds 1. The front is always the current answer, in O(1) per query.
Last non-repeating character. Same counts, iterate the string backwards.
First non-repeating word in a document. Identical structure over tokens rather than characters.
What it is testing
Do you use two passes rather than a nested loop? The O(n²) version is the baseline to improve on.
Do you iterate the string in the second pass? Order matters, and the counter's order is an implementation detail to avoid depending on.
Do you know the alphabet-size bound? Stating the space as O(1) for a fixed alphabet, rather than O(n), is the precise answer.
Do you ask about case and non-letter characters?
Do you notice the asymmetry with "first repeating"? Recognising that one is answerable in a single pass and the other is not shows you understand why the two passes are needed.
Recap in one screen
- Count every character, then walk the string in order and return the first index with count 1.
- O(n) time, O(k) space bounded by the alphabet — effectively O(1) for fixed alphabets.
- Two passes are unavoidable: uniqueness is not knowable until the input ends.
- Iterate the string, not the counter, in the second pass.
- "First repeating" is the one-pass sibling, answerable the moment the repeat appears.