Skip to article
ALGORITHMICSDSA / Hashing
DSAeasy4 min read

Unique Email Addresses

Normalise, then count the set — and the one part of the address you must not touch.


In the local part of an address (before the @), dots are ignored and everything from the first + onward is ignored. The domain is used as-is. How many distinct addresses are there?

numUniqueEmails([]string{
"test.email+alex@leetcode.com",
"test.e.mail+bob.cathy@leetcode.com",
"testemail+david@lee.tcode.com",
}) // 2

Normalise, then count

There is no algorithm here. The whole problem is applying two rules to the right half of the string and putting the results in a set.

1 / 9

test.email+alex@leetcode.com

test.email+alex@leetcode.com

distinct addresses — 0

Split at the @. Only the local part gets normalised — the domain is left exactly as it is.

func numUniqueEmails(emails []string) int {
emailSet := make(map[string]struct{})
for _, email := range emails {
parts := strings.Split(email, "@")
local, domain := parts[0], parts[1]
local = strings.Split(local, "+")[0]
local = strings.ReplaceAll(local, ".", "")
emailSet[local+"@"+domain] = struct{}{}
}
return len(emailSet)
}

O(total characters)O(\text{total characters}), and map[string]struct{} is Go’s idiom for a set.

What Split is doing

strings.Split(email, "@") returns every field, and the code takes parts[1]. That assumes exactly one @, which the constraints guarantee. Without the guarantee, "a@b@c.com" would silently use "b" as the domain and drop the rest.

strings.Cut states the assumption and handles its absence:

local, domain, ok := strings.Cut(email, "@")
if !ok {
continue // or return an error — but now the case is visible
}

Cut splits on the first occurrence and reports whether it found one. It also allocates nothing, where Split builds a slice. Since Go 1.18 it is the right default for “split into two at a separator”.

Likewise strings.Split(local, "+")[0] builds every +-separated field to use the first; local, _, _ = strings.Cut(local, "+") builds none.