Naming
PEP 8: The Style Guide for Python Code has recommendations all Python programs should follow. One of these is function names should be in snake_case
. As such, isUnique
should actually be named is_unique
.
Don't shadow built-in function names
chr
is a built-in function. For example, chr(97)
returns 'a'
.
After executing for chr in string
, you no longer have access to the chr
function in that scope. ch
is commonly used as a variable for extracting characters from a string.
for-else
The for ... else: ...
construct is for use when you use break
to terminate a loop early if something is found, executing the else:
portion only if the search failed to find a result.
In this particular case, you are not using break
; rather you return
from inside the loop. In such as situation, else:
is unnecessary, and can cause confusion. Consider:
def example(container):
for item in container:
if complicated_test(item):
return True
else:
print("Point A")
print("Point B")
return False
There is no way "Point B"
can be reached without also reaching "Point A"
first. The else:
is an unnecessary control block.
Magic numbers
What is 95
? Where did it come from? It is a magic number. My first guess was that it is the ordinal of 'a'
, but that turned out to be wrong.
The constant deserves a name. LOWERCASE_TO_MERSENNE_OFFSET
comes to mind as a possibility, though it might be a bit long. You might even want to define it with an expression, to help readers see where it comes from:
LOWERCASE_TO_MERSENNE_OFFSET = ord('a') - 2
Binary numbers
You are trying to use prime numbers to store flags in a single integer, to indicate whether or not a lowercase letter has been seen. Using bits to store these flags in a single integer is much simpler.
\$2^0\$ would be the 'a'
flag, \$2^1\$ would be the 'b'
flag, ... \$2^{25}\$ would be the 'z'
flag
def is_unique(string: str) -> bool:
"""Determine if a string contains unique lowercase letters
Returns `True` if all lowercase letters are unique, `False` otherwise.
Calling the function with `string` containing anything other than
lowercase letters results in undefined behaviour.
"""
letter_flags = 0
first = ord('a')
for ch in string:
flag = 1 << (ord(ch) - first)
if letter_flags & flag:
return False
letter_flags |= flag
return True
Since larger integers in Python are stored as objects on the heap, and are immutable, bit manipulation requires creating a new object when the bits of the integer are changed. As such, bit manipulation in Python is not as fast as in languages like C, C++, or Java.
There is a bitarray
package which can be installed (pip install bitarray
) which may be used to create mutable bit arrays. Using a bitarray
instead of an integer will be much faster, yet still keeps the memory footprint of the application near its absolute minimum. Since a bitarray
can be thought of as the bits of an integer, this can still be thought of as storing your “seen flags” in a single integer.
from bitarray import bitarray
def is_unique(string: str) -> bool:
"""Determine if a string contains unique lowercase letters
Returns `True` if all lowercase letters are unique, `False` otherwise.
Calling the function with `string` containing anything other than
lowercase letters results in undefined behaviour.
"""
letter_flags = bitarray(26)
letter_flags.setall(False)
first = ord('a')
for ch in string:
flag = ord(ch) - first
if letter_flags[flag]:
return False
letter_flags[flag] = True
return True
Finally, bit manipulation will always incur an overhead over direct indexing. Using a bytearray(26)
object to hold the twenty-six flags is likely faster than using a bitarray
. It is no longer meeting your implied goal of storing the flags inside a single integer. It requires perhaps 22 additional bytes of memory, but does not require installation of an external package.
def is_unique(string: str) -> bool:
"""Determine if a string contains unique lowercase letters
Returns `True` if all lowercase letters are unique, `False` otherwise.
Calling the function with `string` containing anything other than
lowercase letters results in undefined behaviour.
"""
letter_flags = bytearray(26)
first = ord('a')
for ch in string:
flag = ord(ch) - first
if letter_flags[flag]:
return False
letter_flags[flag] = 1
return True
Set
Juho's set solution is a simple 1-line solution, but it is \$O(N)\$ in time. With certain inputs, it can take a very long time, failing programming challenges.
Eg) is_unique('a' * 1_000_000_000)
calls the function with a string 1 billion characters long, and then iterates over the entire string to build the set. If you want to use this type of solution, you should include a fast fail to catch these types of degenerate cases:
from string import ascii_lowercase
def is_unique(string: str) -> bool:
# Pigeon hole principle: a string longer than 26 characters must have duplicates!
if len(string) > len(ascii_lowercase):
return False
return len(string) == len(set(string))
2**i - 1
, the code would work. It would become really slow, e.g. with a long string of unique unicode chars, but it should work as far as I can tell. \$\endgroup\$