Though the variable names could be better, I'm going to focus on data structures and function interfaces in my answer.
Use std::bitset
to manage your… well… bit set
The <bitset>
header declares the std::bitset
class template:
template< std::size_t N >
class bitset;
You would use it like this:
std::bitset<1 << CHAR_BIT> seen;
...
if (seen[c])
return false;
seen[c] = true;
This implementation can test the uniqueness of all one-byte-characters (but it doesn't need to if you restrict or transform the character set like you do already).
If you want to save a little memory (8 bytes vs. 32 bytes) and gain a little performance (1 shift and 1 AND per look-up if your compiler is smart enough), e. g. because you only want test for uniqueness of case-insensitive ASCII letters, you can restrict the bit set size to 'Z' - 'A' + 1
and do your thing. I doubt that this will have a noticeable impact on performance except for very long strings though.
Use iterators instead of containers to work on sequences
This makes your algorithm much more useful if someone wants to use it on other character sequence containers.
#include <bitset>
#include <string>
#include <iostream>
template <class Iter>
bool is_unique(Iter begin, Iter end) {
std::bitset<1 << CHAR_BIT> seen;
for (; begin != end; begin++) {
char c = *begin;
...
unsigned char uc = static_cast<unsigned char>(c);
if (seen[uc])
return false;
seen[uc] = true;
}
return true;
}
int main(int argc, char **argv) {
std::string s(argv[1]);
std::cout << is_unique(s.begin(), s.end()) << std::endl;
}
You could even overload the function for null-terminated C strings:
#include <cstring>
bool is_unique(const char *s) {
return is_unique(s, s + std::strlen(s));
}
int main(int argc, char **argv) {
std::cout << is_unique(argv[1]) << std::endl;
}
Support arbitrary character types
Since we're using templated iterators already, we can just as well generalize the character types.
#include <bitset>
#include <string>
#include <iterator>
#include <type_traits>
#include <climits>
#include <iostream>
template <class Iter>
bool is_unique(Iter begin, Iter end) {
typedef typename std::iterator_traits<Iter>::value_type char_type;
typedef std::char_traits<char_type> char_traits;
typedef typename std::make_unsigned<typename char_traits::int_type>::type uint_type;
static_assert(sizeof(char_type) < sizeof(std::size_t));
std::bitset<std::size_t(1) << sizeof(char_type)*CHAR_BIT> seen;
for (; begin != end; begin++) {
char_type c = *begin;
// ...
uint_type uc = static_cast<uint_type>(char_traits::to_int_type(c));
if (seen[uc])
return false;
seen[uc] = true;
}
return true;
}
template <class CharT>
bool is_unique(const CharT *s) {
return is_unique(s, s + std::char_traits<CharT>::length(s));
}
Be aware that the bit set will grow very quickly if you use wider character types, e. g. to 8 KiB for wchar_t
with 16 bits (Windows) or a whooping 512 MiB for wchar_t
with 32 bits (*nix). In those cases you probably want to restrict the tested character subset and/or place the bitset
on the heap instead of the stack and/or use an unordered_set
that can grow dynamically according to the set of actually occurring characters¹.
¹ Using an unordered_set
based on character width
Using an unordered_set
for large character widths (and alphabets) is smart because a string of length N can hold no more than N different characters, so the the memory consumption of an unordered_set
is within \$O(\min\{N, |\Sigma|\})\$ instead of the bitset
's \$O(|\Sigma|)\$ (Σ being the alphabet), which means the entire program only needs up to \$O(N + \min\{N, |\Sigma|\}) = O(N)\$ memory units (one additional N for the input).
You can use partial template specialization to use either bitset
or unordered_set
based on the character width like this:
#include <bitset>
#include <unordered_set>
#include <string>
#include <iterator>
#include <type_traits>
#include <climits>
#include <iostream>
#include <locale>
#include <codecvt>
#define MAX_STATIC_CHARSET_SIZE (8 << 20)
template <class CharT, bool StaticSetSize>
class my_charset_impl;
template <class CharT>
class my_charset_impl<CharT, true> {
private:
typedef CharT char_type;
typedef std::char_traits<char_type> char_traits;
typedef typename std::make_unsigned<typename char_traits::int_type>::type uint_type;
static_assert(sizeof(char_type) < sizeof(std::size_t));
std::bitset<std::size_t(1) << sizeof(char_type)*CHAR_BIT> _set;
public:
bool insert(char_type c) {
auto entry_ref = _set[static_cast<uint_type>(char_traits::to_int_type(c))];
if (entry_ref)
return false;
entry_ref = true;
return true;
}
};
template <class CharT>
class my_charset_impl<CharT, false> {
private:
typedef CharT char_type;
std::unordered_set<char_type> _set;
public:
bool insert(char_type c) {
return _set.insert(c).second;
}
};
template <class CharT>
using my_charset = my_charset_impl<CharT, sizeof(my_charset_impl<CharT, true>) <= MAX_STATIC_CHARSET_SIZE>;
template <class Iter>
bool is_unique(Iter begin, Iter end) {
typedef typename std::iterator_traits<Iter>::value_type char_type;
my_charset<char_type> seen;
for (; begin != end; begin++) {
char_type c = *begin;
// ...
if (!seen.insert(c))
return false;
}
return true;
}
template <class CharT>
bool is_unique(const CharT *s) {
return is_unique(s, s + std::char_traits<CharT>::length(s));
}
int main(int argc, char **argv) {
std::string s(argv[1]);
std::cout << is_unique(s.cbegin(), s.cend()) << std::endl;
std::cout << is_unique(argv[1]) << std::endl;
std::u32string ws = std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t>().from_bytes(s);
std::cout << is_unique(ws.cbegin(), ws.cend()) << std::endl;
}
This example calls is_unique
three times,
- with
const char *
,
- with
std::string::iterator_type
, and
- with
std::u32string::iterator_type
, where the u32string
instance contains the UTF-32 representation of argv[1]
.
int offset = c & 0x1F;
, which will yield only 0..31 values, and for 'A'..'Z' vs 'a'..'z' it will produce the same values too (functioning as faketolower
). I also personally prefer non-else/non-nested variants, so I would doif (seen & shifted) return false;
and theseen |= shifted;
would go after that in thefor
loop. (this is level golfing comment, i.e. lowering readability of source for some hypothetical performance gain, not suitable for real world production code ;) ). \$\endgroup\$length == zero/one ? return true
andlength > 26 ? return false
since those two circumstances are self explanatory? Can't have duplicates in an empty or one length string... Must have duplicates in a string of 26+ characters, since there are only 26 letters in the alphabet (case insensitive). \$\endgroup\$for(auto c: s)
for free (and you can't get rid of it). The test for one and 26+ length is a nice catch. In real world it doesn't make much sense (as the whole source), because you can rarely find such short/long word, but I didn't even though of that. \$\endgroup\$