Comparing two maps is linear, so that's not a big problem.
I don't think you've made particularly good use of maps though. As I see it, you have two choices. You could use an unordered_multiset
, or you could use a unordered_map<char, size_t>
. In the latter case, you'd keep a count of each character in the string, and increment the count for each character as you scan through the input strings.
Personally, I think it's generally simpler to just sort the two strings, then compare them:
bool is_permutation(std::string s1, std::string s2) {
if (s1.length() != s2.length())
return false;
std::sort(s1.begin(), s1.end());
std::sort(s2.begin(), s2.end());
return s1 == s2;
}
A version making good use of an unordered_map
:
bool is_perm2(std::string const &a, std::string const &b) {
std::unordered_map<char, std::size_t> s1;
std::unordered_map<char, std::size_t> s2;
if (a.length() != b.length())
return false;
for (unsigned char c : a)
++s1[c];
for (unsigned char c : b)
++s2[c];
return s1 == s2;
}
...will gain a substantial advantage if the strings involved are long. Don't expect miracles though--on typical strings (a few dozen to a few hundred characters) overhead often matters more than computational complexity, so this is unlikely to accomplish much. On the other hand, if you honestly expect to process strings of (say) a megabyte or more on a fairly regular basis, you can expect it to run significantly faster than the others.
I have not, however, gotten nearly as good of results from using a multiset
or unordered_multiset
. In fact, both are quite slow in my testing. I suspect the problem is that for this case, they're both storing a lot of data (a node for each character in the input), so before the string is large enough for computational complexity to matter much, they're overflowing the cache, so you end up with a lot of references to main memory. Note that when used as in the question, std::unordered_multimap
ends up similarly (or probably even worse).
For anybody who might care, here's a chart of how various approaches work out for different sizes of strings:
[This shows the log of the time on the vertical axis, to help keep things visible across a wide range of sizes (and with them, times).]
So this shows the unordered_map
having pretty high overhead, but nice, slow growth, so it's the slowest for small strings, but the fastest for strings over 8K in size.
Sorting is somewhat similar--faster for strings up to a kilobyte, but slower than the unordered_map
for larger strings.
Using a raw array for the counts has behavior similar to sort, but even more dramatic--very fast for small strings, but slows down quite a lot for larger strings.
There seems to be no size at which the set/unordered_set (or unordered_multimap as used in the question) be the optimal choice.
Oh, for anybody who wonders about the odd stair-step look: I generated two tests for strings of each length: one with the strings identical, one with one character changed between them (i.e., one that should return true and another than should return false). I suppose if I wanted to be really complete, I should also add a test case for strings that are shuffled versions of each other, so they are unequal, but should still return true, but I haven't bothered with that (I have tested to be reasonably certain all the charted algorithms actually work though).
Edit 2:
After more testing, I have to agree with the consensus that counting characters in an array is a better choice:
bool is_perm3(std::string const &a, std::string const &b) {
std::array<std::size_t, std::numeric_limits<unsigned char>::max()+1> s1;
std::array<std::size_t, std::numeric_limits<unsigned char>::max()+1> s2;
if (a.length() != b.length())
return false;
for (unsigned char c : a)
++s1[c];
for (unsigned char c : b)
++s2[c];
for (std::size_t i=0; i<s1.size(); i++)
if (s1[i] != s2[i])
return false;
return true;
}
I remain puzzled (and frankly, rather bothered) but the poor performance of std::vector
vs. std::array
in his test. We'd expect array
to have less initial overhead, but the vector
was pre-allocated, so speed differences after being set up should be quite minimal (but in this case definitely are not).
return std::is_permutation(s1.begin(), s1.end(), s2.begin(), s2.end());
? Or should this be tagged reinventing-the-wheel? \$\endgroup\$