Task
I was asked to write a function, which can determine (return a bool) whether 2 given strings are anagrams of each other? That function should have the following constraints:
- Upper and lowercase letters are considered the same
- Non-letters (so numbers, punctuation, whitespace) should be ignored
My implementations:
I know there might be more improvments to my implementations, but I have questions regarding those implementations
- The Version1 iterates over two strings till the end. The
processChar
increases/decreses the counter of a letter. - The Version2 avoids unnecessary processing of the rest of the second string if one of the strings ends first and there is at least one NEW char, in second string, that is not already in the hash table. E.g., s1="anag", s2="1234pgana". Once
s1
ends (the iterator of s2 is on 'p' now), there is no point processing the rest of s2 since it has new char 'p' which is not in hash table.
Feedback from interviewer:
- He does not understand if there is any benefit from Version2.
- He's not sure if there is any benefit processing both strings in parallel VS iterating over each of them separately (one at a time). He wonders about very large strings and how this might impact caching. "Assume that the 2 strings are so large that they won't fit into the L1 cache (either of them). So by traversing both of them in parallel I am a bit concerned that we might not get the most efficient caching behavior, as in my opinion, it would be preferable to access the memory sequentially. So one string at a time. I do however admit that I cannot say conclusively whether this hypothetical scenario is actually an issue."
My questions
Is there really no benefit from Version2. I had another Version3 that traverses the strings from start and end simultaneously to make it O(n/2) instead of O(n).
I've never encountered with such cache problem like he explained. I think we need a profiler to check if it's really a problem. Especially when he admitted that he does not know if that's could be an issue.
Version1
bool isAnagram1(const std::string& s1, const std::string& s2)
{
std::unordered_map<char, int> charCounter;
auto processChar = [&](char c, int inc)
{
// increase/decrease counter if a letter
if (isalpha(c))
{
c = tolower(c);
charCounter[c] += inc;
if (charCounter[c] == 0)
charCounter.erase(c);
}
};
for (int i = 0; i < s1.length() || i < s2.length(); i++)
{
if (i < s1.length())
processChar(s1[i], 1);
if (i < s2.length())
processChar(s2[i], -1);
}
return charCounter.empty();
}
Version2
bool isAnagram2(const std::string& s1, const std::string& s2)
{
if (s1.length() == 0 && s2.length() == 0)
return true;
std::unordered_map<char, int> charCounter;
auto processChar = [&](char c, int inc)
{
// increase/decrease counter if a letter
if (isalpha(c))
{
c = tolower(c);
charCounter[c] += inc;
if (charCounter[c] == 0)
charCounter.erase(c);
}
};
auto processIfCharNew = [&](char c, int inc)
{
// if not a letter, then ignore it.
if (!isalpha(c))
return true;
// if a new letter, then it's not anagram
c = tolower(c);
if (charCounter.find(c) == charCounter.end())
return false;
// if an old letter, then increase/decrease counter of a letter
charCounter[c] += inc;
if (charCounter[c] == 0)
charCounter.erase(c);
return true;
};
auto s1Inc = 1;
auto s2Inc = -1;
auto i = 0;
while (i < s1.length() && i < s2.length())
{
processChar(s1[i], s1Inc);
processChar(s2[i], s2Inc);
++i;
}
// if both strings ended then it's anagram if char counter is empty
if (i == s1.length() && i == s2.length())
return charCounter.empty();
// if one of the strings finished first, then we need to continue processing the second string ONLY until the end OR until a new char is found. In the later case it means it's not anagram
auto isFinishedS1 = i == s1.length();
auto s3Inc = isFinishedS1 ? s2Inc : s1Inc;
decltype(auto) s3 = isFinishedS1 ? s2 : s1;
auto res = true;
while (i < s3.length())
{
if (!(res = processIfCharNew(s3[i], s3Inc)))
break;
++i;
}
return charCounter.empty() && res;
}
O(n)
as constant factor is ignored. (and it seems you would to twice more work by iteration (on half range), so2 * (1/2)
==1
). \$\endgroup\$