3860. Unique Email Groups 🔒
Problem Description
You are given an array of strings emails, where each string is a valid email address. Your task is to find out how many unique email groups exist after applying a set of normalization rules.
Two email addresses belong to the same group if both their normalized local names and normalized domain names are identical.
Every email address is made up of two parts separated by the '@' symbol:
- The local name is the part before the
'@'. - The domain name is the part after the
'@'.
The normalization rules are as follows:
- For the local name:
- Ignore all dots
'.'(remove them). - Ignore everything after the first
'+', if a'+'is present (keep only the part before it). - Convert the remaining characters to lowercase.
- Ignore all dots
- For the domain name:
- Convert it to lowercase.
After normalizing each email address, two emails are considered to be in the same group when their normalized local name and normalized domain name match exactly.
Return an integer denoting the number of unique email groups after normalization.
For example, the local name Test.Email+spam normalizes to testemail (dots removed, the +spam suffix dropped, and converted to lowercase). The domain name LeetCode.com normalizes to leetcode.com. Two emails sharing the same normalized local name and domain name count as one group.
How We Pick the Algorithm
Why Hash Table / Counting?
This problem maps to Hash Table / Counting through a short path in the full flowchart.
Normalize each email by applying rules then count distinct normalized strings via a hash set.
Open in FlowchartIntuition
The key observation is that two email addresses belong to the same group only after they have been normalized. So instead of comparing the original strings directly, we first transform each email into its canonical (normalized) form, and then any two emails that map to the same canonical form belong to the same group.
Once every email is reduced to a single, consistent normalized string, the problem becomes very simple: we just need to count how many distinct normalized strings there are. This naturally leads us to use a hash set, since a set automatically discards duplicates and keeps only unique values.
So the approach forms two steps:
- Normalize each email by applying the rules: for the local name, remove all dots
'.', drop everything after the first'+', and convert to lowercase; for the domain name, just convert to lowercase. Then join them back together into one string. - Add each normalized string to a set. Because the set stores only unique entries, emails that end up identical after normalization collapse into a single element.
Finally, the size of the set is exactly the number of unique email groups, since each distinct element in the set represents one group.
Solution Approach
Solution 1: Hash Table
We use a hash set st to store the normalized result of each email address. For each email address, we normalize it according to the problem requirements, then add the result to the set. Since a set keeps only unique elements, its final size gives us the number of unique email groups.
The detailed steps are as follows:
-
Split the email into a local name and a domain name using the
'@'symbol. In the code this is done withlocal, domain = email.split("@"). -
Normalize the local name:
- First, drop everything after the first
'+'by takinglocal.split("+")[0], which keeps only the part before the plus sign (or the whole string if no'+'exists). - Then remove all dots with
.replace(".", ""). - Finally convert to lowercase with
.lower().
- First, drop everything after the first
-
Normalize the domain name by simply converting it to lowercase with
domain.lower(). -
Concatenate the normalized local name and domain name into a single string
normalized = local + domain, and add it to the hash setst.
After processing all emails, the number of elements in the hash set st is the number of unique email groups, returned with len(st).
Complexity Analysis:
- Time complexity:
O(L), whereLis the total length of all the characters inemails. Each email is scanned a constant number of times during splitting, replacing, and lowercasing. - Space complexity:
O(L), since in the worst case the hash set stores normalized strings whose combined length is proportional to the input.
class Solution:
def uniqueEmailGroups(self, emails: list[str]) -> int:
st = set()
for email in emails:
local, domain = email.split("@")
local = local.split("+")[0].replace(".", "").lower()
domain = domain.lower()
normalized = local + domain
st.add(normalized)
return len(st)
Example Walkthrough
Let's trace through the solution approach using a small example:
emails = [ "Test.Email+spam@LeetCode.com", "test.e.mail+bob.cathy@leetcode.com", "TestEmail+david@lee.tcode.com" ]
We initialize an empty hash set st = {} and process each email one at a time.
Email 1: "Test.Email+spam@LeetCode.com"
- Split on
'@':local = "Test.Email+spam",domain = "LeetCode.com". - Normalize the local name:
- Drop everything after the first
'+':"Test.Email+spam".split("+")[0]→"Test.Email". - Remove all dots:
"Test.Email".replace(".", "")→"TestEmail". - Convert to lowercase:
"TestEmail".lower()→"testemail".
- Drop everything after the first
- Normalize the domain name:
"LeetCode.com".lower()→"leetcode.com". - Concatenate:
"testemail" + "leetcode.com"→"testemailleetcode.com". - Add to set:
st = {"testemailleetcode.com"}.
Email 2: "test.e.mail+bob.cathy@leetcode.com"
- Split on
'@':local = "test.e.mail+bob.cathy",domain = "leetcode.com". - Normalize the local name:
- Drop after
'+':"test.e.mail+bob.cathy".split("+")[0]→"test.e.mail". - Remove dots:
"test.e.mail".replace(".", "")→"testemail". - Lowercase:
"testemail"(already lowercase).
- Drop after
- Normalize the domain name:
"leetcode.com"(already lowercase). - Concatenate:
"testemail" + "leetcode.com"→"testemailleetcode.com". - Add to set: This string already exists, so the set is unchanged:
st = {"testemailleetcode.com"}.
Notice how emails 1 and 2 looked very different originally but collapsed to the same normalized form — this is exactly the behavior we want, and the set handles the duplicate automatically.
Email 3: "TestEmail+david@lee.tcode.com"
- Split on
'@':local = "TestEmail+david",domain = "lee.tcode.com". - Normalize the local name:
- Drop after
'+':"TestEmail+david".split("+")[0]→"TestEmail". - Remove dots:
"TestEmail"(no dots). - Lowercase:
"testemail".
- Drop after
- Normalize the domain name:
"lee.tcode.com".lower()→"lee.tcode.com". - Concatenate:
"testemail" + "lee.tcode.com"→"testemaillee.tcode.com". - Add to set: This is a new string (the domain differs), so:
st = {"testemailleetcode.com", "testemaillee.tcode.com"}.
Even though email 3 has the same normalized local name as the others (testemail), its domain is lee.tcode.com, not leetcode.com. Since a group requires both parts to match, this email forms its own group.
Final Result
After processing all three emails, the set contains:
{ "testemailleetcode.com", "testemaillee.tcode.com" }
The size of the set is 2, so we return len(st) = 2. There are 2 unique email groups: emails 1 and 2 form one group, and email 3 forms another.
Solution Implementation
1class Solution:
2 def uniqueEmailGroups(self, emails: list[str]) -> int:
3 # Set to store distinct normalized email addresses
4 unique_emails: set[str] = set()
5
6 for email in emails:
7 # Split into local name and domain name on the single '@'
8 local, domain = email.split("@")
9
10 # In the local part:
11 # - everything after the first '+' is ignored (alias),
12 # - all '.' characters are removed,
13 # - normalize case to lowercase for consistent comparison
14 local = local.split("+")[0].replace(".", "").lower()
15
16 # Domain part only needs case normalization
17 domain = domain.lower()
18
19 # Rebuild the canonical address and record it
20 normalized_email = local + "@" + domain
21 unique_emails.add(normalized_email)
22
23 # Number of distinct addresses equals the size of the set
24 return len(unique_emails)
251class Solution {
2 public int uniqueEmailGroups(String[] emails) {
3 // Use a set to store the normalized email addresses,
4 // since a set automatically discards duplicates.
5 Set<String> uniqueEmails = new HashSet<>();
6
7 for (String email : emails) {
8 // Split each email into the local name and the domain name
9 // using '@' as the separator.
10 String[] parts = email.split("@");
11 String localName = parts[0];
12 String domainName = parts[1];
13
14 // If a '+' appears in the local name, everything after it
15 // (including the '+') is ignored.
16 int plusIndex = localName.indexOf('+');
17 if (plusIndex != -1) {
18 localName = localName.substring(0, plusIndex);
19 }
20
21 // Remove all dots from the local name, as they are ignored,
22 // and convert it to lower case to make the comparison case-insensitive.
23 localName = localName.replace(".", "").toLowerCase();
24
25 // Normalize the domain name to lower case as well.
26 domainName = domainName.toLowerCase();
27
28 // Combine the processed local name and domain name to form the
29 // canonical email, then add it to the set.
30 String normalizedEmail = localName + "@" + domainName;
31 uniqueEmails.add(normalizedEmail);
32 }
33
34 // The size of the set is the number of distinct email addresses.
35 return uniqueEmails.size();
36 }
37}
381class Solution {
2public:
3 int uniqueEmailGroups(vector<string>& emails) {
4 // Set to store the normalized form of each email; duplicates are ignored.
5 unordered_set<string> uniqueAddresses;
6
7 for (const auto& email : emails) {
8 // Split the email into local and domain parts at the '@' separator.
9 size_t atPos = email.find('@');
10 string localPart = email.substr(0, atPos);
11 string domainPart = email.substr(atPos + 1);
12
13 // In the local part, anything after a '+' is ignored.
14 size_t plusPos = localPart.find('+');
15 if (plusPos != string::npos) {
16 localPart = localPart.substr(0, plusPos);
17 }
18
19 // Build the cleaned local part: drop '.' characters and lowercase the rest.
20 string cleanedLocal;
21 for (char c : localPart) {
22 if (c != '.') {
23 cleanedLocal += static_cast<char>(tolower(c));
24 }
25 }
26
27 // Lowercase the domain part for case-insensitive comparison.
28 for (char& c : domainPart) {
29 c = static_cast<char>(tolower(c));
30 }
31
32 // Insert the normalized "local@domain" form into the set.
33 uniqueAddresses.insert(cleanedLocal + "@" + domainPart);
34 }
35
36 // The number of distinct entries equals the number of unique email groups.
37 return static_cast<int>(uniqueAddresses.size());
38 }
39};
401/**
2 * Counts the number of unique email groups after normalization.
3 *
4 * Normalization rules for each email (split into local and domain by '@'):
5 * - Local part: ignore everything after a '+', remove all '.' characters,
6 * and convert to lowercase.
7 * - Domain part: convert to lowercase.
8 *
9 * Two emails belong to the same group if their normalized forms are identical.
10 *
11 * @param emails - The list of email addresses to process.
12 * @returns The count of distinct normalized emails.
13 */
14function uniqueEmailGroups(emails: string[]): number {
15 // Stores each normalized email; duplicates are automatically discarded.
16 const uniqueEmails: Set<string> = new Set<string>();
17
18 for (const email of emails) {
19 // Separate the email into its local and domain components.
20 let [localPart, domainPart] = email.split('@');
21
22 // Normalize the local part:
23 // 1. Drop any characters after the first '+'.
24 // 2. Remove all '.' characters.
25 // 3. Convert to lowercase.
26 localPart = localPart
27 .split('+')[0]
28 .replace(/\./g, '')
29 .toLowerCase();
30
31 // Normalize the domain part by lowercasing it.
32 domainPart = domainPart.toLowerCase();
33
34 // Combine the normalized parts into a single canonical key.
35 const normalizedEmail: string = localPart + domainPart;
36
37 // Record the canonical form; the Set ignores repeats.
38 uniqueEmails.add(normalizedEmail);
39 }
40
41 // The size of the Set equals the number of unique email groups.
42 return uniqueEmails.size;
43}
44Time and Space Complexity
-
Time Complexity:
O(n · m), wherenis the number of email addresses andmis the average length of each email address. The algorithm iterates over allnemails, and for each one performs string operations such assplit("@"),split("+"),replace(".", ""), andlower(). Each of these operations processes the characters of the email, costingO(m)per email. Building the normalized string and inserting it into the set also takesO(m)on average. Therefore, the total time complexity isO(n · m). -
Space Complexity:
O(n · m). In the worst case, all email addresses are distinct after normalization, so the setststores up tonnormalized strings, each of length up toO(m). This results inO(n · m)space. Additionally, the intermediate strings created during each iteration (local,domain,normalized) takeO(m)space, which is dominated by the set's storage. Hence, the overall space complexity isO(n · m).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting a separator when concatenating the local and domain names
A subtle but serious bug appears in the Solution Approach above: it builds the key as normalized = local + domain without any separator between the two parts. This can cause two genuinely different emails to collapse into the same group.
Consider these two emails:
"ab@c.com"→ local"ab", domain"c.com"→ key"abc.com""a@bc.com"→ local"a", domain"bc.com"→ key"abc.com"
These are clearly different addresses, yet both produce the identical key "abc.com", so they would be wrongly counted as one group.
Solution: Always insert the '@' (or any character that cannot appear in a normalized local name) between the two parts when forming the key. The final Python Code already does this correctly with normalized_email = local + "@" + domain. Keep that separator:
normalized_email = local + "@" + domain unique_emails.add(normalized_email)
Pitfall 2: Applying the local-name rules to the domain name
It is easy to accidentally strip dots or cut off '+' from the domain as well. The dot-removal and '+'-truncation rules apply only to the local name. Removing dots from the domain would merge "leetcode.com" and "leetcodecom", and would also corrupt legitimate domains.
Solution: Normalize the domain with lowercasing only:
local = local.split("+")[0].replace(".", "").lower() # full rules domain = domain.lower() # lowercase only
Pitfall 3: Splitting on '@' incorrectly
Using a plain split("@") is fine for valid emails (exactly one '@'), but if you ever generalize the input, multiple '@' characters would raise a ValueError: too many values to unpack. Likewise, ordering matters: you must drop the '+' suffix before removing dots, otherwise a '+' could survive inside the part you keep.
Solution: Rely on the guarantee that each email is valid (exactly one '@'), and if you want to be defensive, split with a limit so the domain keeps any stray characters:
local, domain = email.split("@", 1) # split only on the first '@'
Pitfall 4: Case sensitivity handled too late or only partially
If you compare or store the key before calling .lower(), addresses differing only in case ("Test@X.com" vs "test@x.com") are treated as distinct. Both local and domain require lowercasing.
Solution: Apply .lower() to both parts during normalization, as shown, so the comparison is fully case-insensitive.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhich type of traversal does breadth first search do?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Don’t Miss This!