As has been pointed out in the comments and @Kraigolas's answer, the existing algorithm doesn't work. Here's why.
In the string "xxy"
:
- The algorithm first considers the third character,
"y"
. It finds it has not encountered it before, so records it as a preliminary out
value. It adds it to smap
with value 1
.
- The algorithm next considers the second character,
"x"
. It finds it has not encountered this character before, either. It discards its previous preliminary out
value, "y"
, and adopts "x"
as its new preliminary out
value. It adds "x"
to smap
with value 1
.
- The algorithm now moves on to the first character,
"x"
. It realises that it has seen this one before, so adds 1
to the associated value in smap
.
- The algorithm has now finished iterating through the loop. Its current
out
value is "x"
, but when it looks it up in smap
, it finds it has encountered "x"
more than once. As a result, it assumes that there are 0 characters that it has seen only once, and returns None
.
It is impossible to fix the current implementation of the algorithm while keeping to the condition that there should be "only a single loop". If you were to relax this restriction, you could "fix" your current algorithm like this:
def first_non_repeater(string):
smap = {}
for char in string:
if char in smap:
smap[char] += 1
else:
smap[char] = 1
return next((k for k, v in smap.items() if v == 1), None)
Though it's generally both more idiomatic and more efficient, in python, to "ask for forgiveness" rather than "ask for permission":
def first_non_repeater(string):
smap = {}
for char in string:
try:
smap[char] += 1
except KeyError:
smap[char] = 1
return next((k for k, v in smap.items() if v == 1), None)
And you can get there more cleanly by just using a defaultdict
:
from collections import defaultdict
def first_non_repeater(string):
smap = defaultdict(int)
for char in string:
smap[char] += 1
return next((k for k, v in smap.items() if v == 1), None)
And I like @FMc's answer using collections.Counter
more than any of these three — I think using Counter
is probably the most pythonic way of doing this.
But at I say, all of these solutions break the condition that only one loop is allowed (whether that's the key concern when it comes to an efficient algorithm in python is another question).
The following would be my attempt at a solution that only uses builtin data structures (nothing from collections
), and that only uses a single loop. The solution only works on python 3.6+, as it relies on dict
s being ordered. Feedback welcome:
from typing import TypeVar, Union, Optional
T = TypeVar('T')
def first_non_repeating_char(
string: str,
default: Optional[T] = None
) -> Union[str, T, None]:
"""Return the first non-repeating character in a string.
If no character in the string occurs exactly once,
return the default value.
Parameters
----------
string: str
The string to be searched.
default: Any, optional
The value to be returned if there is no character
in the string that occurs exactly once.
By default, None.
Returns
-------
Either a string of length 1, or the default value.
"""
# Using a dictionary as an ordered set,
# for all the characters we've seen exactly once.
# The values in this dictionary are irrelevant.
uniques: dict[str, None] = {}
# a set for all the characters
# that we've already seen more than once
dupes: set[str] = set()
for char in string:
if char in dupes:
continue
try:
del uniques[char]
except KeyError:
uniques[char] = None
else:
dupes.add(char)
# return the first key in the dictionary
# if the dictionary isn't empty.
# If the dictionary is empty,
# return the default.
return next(iter(uniques), default)
collections
/itertools
etc okay? \$\endgroup\$