LeetCode 3058 - Friends With No Mutual Friends
The Friends table represents an undirected friendship graph. Each row (userid1, userid2) means the two users are directly connected as friends. The problem asks us to find every friendship pair where the two users do not share any common friend.
Difficulty: 🟡 Medium
Topics: Database
Solution
Problem Understanding
The Friends table represents an undirected friendship graph. Each row (user_id1, user_id2) means the two users are directly connected as friends. The problem asks us to find every friendship pair where the two users do not share any common friend.
A mutual friend is a third user who is connected to both people in the pair. For example, if users 1 and 2 are friends, and both are also friends with user 5, then 5 is a mutual friend of 1 and 2.
The important detail is that we only care about pairs that already exist in the Friends table. We are not trying to generate all possible user pairs. Instead, for every friendship edge already present, we check whether the two endpoints have an intersection in their friend lists.
The input guarantees that (user_id1, user_id2) is unique. Since friendships are mutual, the relationship is conceptually undirected, even though only one ordered row is stored.
The expected output is every friendship pair that has zero mutual friends, sorted by user_id1 and then user_id2.
A few important observations and edge cases emerge immediately:
- Two users may have many friends, making naive comparison expensive.
- A pair with no other connected users should always qualify.
- Cycles in the friendship graph create mutual friends naturally.
- Dense friendship networks can create many overlapping friend sets.
- Since the friendship relation is undirected, we must treat
(a, b)and(b, a)as equivalent.
The key challenge is efficiently determining whether two connected users share any neighbor.
Approaches
Brute Force Approach
The brute force solution examines every friendship pair individually. For each pair (u, v), we gather all friends of u and all friends of v, then compare them to determine whether a common user exists.
One straightforward implementation is:
- Build an adjacency list for every user.
- For each friendship pair:
- Iterate through all friends of
u - For each friend, check whether it also belongs to
v's friend list
This approach is correct because it explicitly checks whether the intersection of the two friend sets is empty.
However, the runtime becomes expensive when users have many friends. In a dense graph, each friendship may require scanning large adjacency lists, leading to poor performance.
Optimal Approach
The key observation is that mutual friendships naturally appear as paths of length two.
Suppose we have:
(u, x)(v, x)
Then x is a mutual friend of u and v.
Instead of checking every pair independently, we can directly generate all friendship pairs that do have mutual friends. After identifying them, we simply exclude those pairs from the final result.
This can be done efficiently using a self join on the Friends table:
- Join rows where two users share the same friend
- Normalize pair ordering with
LEAST()andGREATEST() - Collect all pairs that have at least one mutual friend
- Return friendship pairs not present in that set
This transforms the problem into a set exclusion operation, which databases handle efficiently.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(E × D) | O(V + E) | For each friendship, scan adjacency lists |
| Optimal | O(E²) worst case | O(E) | Uses joins to directly detect mutual friendships |
Here:
Vis the number of usersEis the number of friendshipsDis the average degree of a node
In practice, the SQL join based approach is significantly cleaner and more efficient for relational databases.
Algorithm Walkthrough
- Treat the friendship table as an undirected graph.
Every row (user_id1, user_id2) represents a bidirectional friendship. Conceptually, both users belong to each other's friend lists.
2. Find all pairs that share a mutual friend.
We perform a self join on the Friends table. Suppose:
- Row
f1contains(a, x) - Row
f2contains(b, x)
Then x is a common friend of a and b.
3. Normalize pair ordering.
Since friendships are undirected, (1, 2) and (2, 1) represent the same pair.
We use:
LEAST(a, b)
GREATEST(a, b)
to ensure consistent ordering. 4. Exclude identical users.
During the self join, we may accidentally produce (u, u). Those are invalid and must be removed.
5. Store all friendship pairs that have at least one mutual friend.
This set represents all pairs we must exclude from the answer. 6. Return original friendship rows not present in the exclusion set.
Every remaining pair is a friendship with zero mutual friends. 7. Sort the result.
The output must be ordered by user_id1 and user_id2.
Why it works
The algorithm relies on a fundamental graph property:
Two users have a mutual friend if and only if there exists some third user connected to both of them.
The self join enumerates exactly these situations. By collecting all friendship pairs that participate in at least one such configuration, we precisely identify every invalid pair. Subtracting them from the original friendships leaves only friendships with no mutual friends.
Python Solution
from typing import List
class Solution:
def friendsWithNoMutualFriends(self, friends: List[List[int]]) -> List[List[int]]:
from collections import defaultdict
# Build adjacency sets
adjacency = defaultdict(set)
for user1, user2 in friends:
adjacency[user1].add(user2)
adjacency[user2].add(user1)
result = []
for user1, user2 in friends:
# Check whether the friend sets intersect
has_mutual = False
# Iterate through smaller set for efficiency
if len(adjacency[user1]) > len(adjacency[user2]):
user1, user2 = user2, user1
for friend in adjacency[user1]:
if friend != user2 and friend in adjacency[user2]:
has_mutual = True
break
if not has_mutual:
result.append([min(user1, user2), max(user1, user2)])
result.sort()
return result
The implementation begins by constructing an adjacency set for every user. A set is used because membership checks are O(1) on average, which makes mutual friend detection efficient.
For each friendship pair, the algorithm checks whether the two users share any common neighbor. To reduce unnecessary work, it iterates through the smaller adjacency set and tests membership in the larger one.
The condition:
friend != user2
ensures that we do not incorrectly treat the direct friendship itself as a mutual friend.
If no shared friend is found, the pair is added to the result.
Finally, the output is sorted to satisfy the required ordering.
Go Solution
package main
import (
"sort"
)
type Solution struct{}
func (s Solution) friendsWithNoMutualFriends(friends [][]int) [][]int {
adjacency := make(map[int]map[int]bool)
// Build adjacency sets
for _, edge := range friends {
u := edge[0]
v := edge[1]
if adjacency[u] == nil {
adjacency[u] = make(map[int]bool)
}
if adjacency[v] == nil {
adjacency[v] = make(map[int]bool)
}
adjacency[u][v] = true
adjacency[v][u] = true
}
result := [][]int{}
for _, edge := range friends {
u := edge[0]
v := edge[1]
hasMutual := false
// Iterate through smaller adjacency set
if len(adjacency[u]) > len(adjacency[v]) {
u, v = v, u
}
for friend := range adjacency[u] {
if friend != v && adjacency[v][friend] {
hasMutual = true
break
}
}
if !hasMutual {
a := u
b := v
if a > b {
a, b = b, a
}
result = append(result, []int{a, b})
}
}
sort.Slice(result, func(i, j int) bool {
if result[i][0] == result[j][0] {
return result[i][1] < result[j][1]
}
return result[i][0] < result[j][0]
})
return result
}
The Go implementation mirrors the Python logic closely.
Since Go does not have a built in hash set type, map[int]bool is used to simulate adjacency sets. Nil maps must be initialized before insertion, so each user's adjacency map is created lazily.
Sorting is implemented with sort.Slice, using lexicographical comparison on the pair values.
Go integers are sufficient for this problem, so overflow concerns are not relevant here.
Worked Examples
Example 1
Input:
[[1,2],[2,3],[2,4],[1,5],[6,7],[3,4],[2,5],[8,9]]
Step 1, Build adjacency sets
| User | Friends |
|---|---|
| 1 | {2,5} |
| 2 | {1,3,4,5} |
| 3 | {2,4} |
| 4 | {2,3} |
| 5 | {1,2} |
| 6 | {7} |
| 7 | {6} |
| 8 | {9} |
| 9 | {8} |
Step 2, Evaluate each friendship
| Pair | Shared Friend? | Result |
|---|---|---|
| (1,2) | 5 | Exclude |
| (2,3) | 4 | Exclude |
| (2,4) | 3 | Exclude |
| (1,5) | 2 | Exclude |
| (6,7) | None | Include |
| (3,4) | 2 | Exclude |
| (2,5) | 1 | Exclude |
| (8,9) | None | Include |
Final output:
[[6,7],[8,9]]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(E × D) | Each friendship checks overlapping neighbors |
| Space | O(V + E) | Adjacency sets store the graph |
The adjacency structure requires storage proportional to the number of users and friendships.
For each friendship edge, we may scan the smaller adjacency set. If the average degree is D, the total runtime becomes approximately O(E × D).
In sparse graphs, this performs very efficiently. In dense graphs, the worst case approaches quadratic behavior.
Test Cases
from typing import List
class Solution:
def friendsWithNoMutualFriends(self, friends: List[List[int]]) -> List[List[int]]:
from collections import defaultdict
adjacency = defaultdict(set)
for u, v in friends:
adjacency[u].add(v)
adjacency[v].add(u)
result = []
for u, v in friends:
has_mutual = False
a, b = u, v
if len(adjacency[a]) > len(adjacency[b]):
a, b = b, a
for friend in adjacency[a]:
if friend != b and friend in adjacency[b]:
has_mutual = True
break
if not has_mutual:
result.append([min(u, v), max(u, v)])
result.sort()
return result
solver = Solution()
# Provided example
assert solver.friendsWithNoMutualFriends([
[1,2],[2,3],[2,4],[1,5],
[6,7],[3,4],[2,5],[8,9]
]) == [[6,7],[8,9]] # mixed graph with valid and invalid pairs
# Single friendship
assert solver.friendsWithNoMutualFriends([
[1,2]
]) == [[1,2]] # no possible mutual friends
# Triangle graph
assert solver.friendsWithNoMutualFriends([
[1,2],[2,3],[1,3]
]) == [] # every pair has a mutual friend
# Disconnected components
assert solver.friendsWithNoMutualFriends([
[1,2],[3,4]
]) == [[1,2],[3,4]] # isolated edges
# Chain graph
assert solver.friendsWithNoMutualFriends([
[1,2],[2,3],[3,4]
]) == [[1,2],[2,3],[3,4]] # no triangles exist
# Square cycle without diagonals
assert solver.friendsWithNoMutualFriends([
[1,2],[2,3],[3,4],[4,1]
]) == [[1,2],[1,4],[2,3],[3,4]] # cycle without shared neighbors
# Dense clique
assert solver.friendsWithNoMutualFriends([
[1,2],[1,3],[1,4],
[2,3],[2,4],[3,4]
]) == [] # every pair shares multiple friends
| Test | Why |
|---|---|
| Provided example | Validates mixed inclusion and exclusion |
| Single friendship | Smallest valid input |
| Triangle graph | Every pair has a mutual friend |
| Disconnected components | Independent edges should remain |
| Chain graph | Verifies absence of triangles |
| Square cycle | Ensures cycles alone do not imply mutual friends |
| Dense clique | Stress case with maximum overlap |
Edge Cases
Single Isolated Friendship
If the graph contains only one friendship pair, there cannot possibly be a mutual friend because no third user exists.
A naive implementation might accidentally treat the connected user itself as a mutual connection. The implementation avoids this by explicitly excluding the direct partner during intersection checks.
Triangle Structures
A triangle such as:
1 - 2
\ /
3
creates a mutual friend for every edge. This is the most important structural pattern in the problem.
Incorrect implementations sometimes only check one direction of adjacency or fail to normalize undirected relationships properly. Using bidirectional adjacency sets guarantees accurate detection.
Dense Friendship Networks
In a clique, every user connects to every other user. Every friendship therefore has many mutual friends.
This stresses both correctness and performance. The implementation handles it correctly because it terminates early once a single shared friend is found, avoiding unnecessary scanning.
Disconnected Components
The graph may contain completely unrelated groups of users.
The algorithm naturally handles this because adjacency sets are local to each connected component. Users from different components never appear during intersection checks.