LeetCode 1398 - Customers Who Bought Products A and B but Not C
The problem asks us to identify customers who meet a very specific purchasing pattern. We are given two tables: Customer
Difficulty: 🟡 Medium
Topics: Database
Solution
Problem Understanding
The problem asks us to identify customers who meet a very specific purchasing pattern. We are given two tables: Customers and Orders. The Customers table contains the unique customer_id and the corresponding customer_name. The Orders table records purchases with order_id, customer_id, and product_name.
Our task is to find customers who bought both product "A" and product "B" but did not buy product "C". This is important for recommending product "C" to these customers. The output should return the customer_id and customer_name of such customers, sorted by customer_id.
The constraints suggest that a customer may have multiple orders, including multiple products, and that customer_id uniquely identifies a customer. Edge cases to consider include customers who bought none of the products, customers who only bought one of "A" or "B", and customers who bought all three products, including "C".
Approaches
Brute-Force Approach
The brute-force approach involves joining the Customers and Orders tables and checking each customer's purchases individually. For each customer, we would enumerate all their products, check if "A" and "B" are present, and confirm "C" is absent. While this works, it can be inefficient because it requires scanning the Orders table for every customer, resulting in multiple passes over the data.
Optimal Approach
The key observation for an optimal approach is that we can aggregate orders by customer and use conditional logic to count purchases of "A", "B", and "C". If the counts show the customer bought at least one "A" and one "B", and zero "C", then the customer qualifies. This uses a single scan of the Orders table grouped by customer_id and is much more efficient.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n*m) | O(n) | Check each customer individually, scanning all orders |
| Optimal | O(n) | O(k) | Aggregate orders by customer_id and use conditional counts |
Here, n is the number of orders, and k is the number of customers.
Algorithm Walkthrough
- Join Customers and Orders: Start by joining the
Customerstable with theOrderstable oncustomer_idto associate each purchase with a customer. - Group by Customer: Aggregate the orders by
customer_idso that each customer becomes a single row for evaluation. - Count Conditional Purchases: For each customer group, count whether "A" and "B" appear at least once and ensure that "C" appears zero times.
- Filter: Only keep customers that satisfy buying "A" and "B" but not "C".
- Select Result: Return the
customer_idandcustomer_nameof the filtered customers, sorted bycustomer_id.
Why it works: By grouping orders per customer and counting purchases conditionally, we ensure that all products bought by a customer are evaluated efficiently. Using aggregation guarantees that each customer is considered exactly once, preserving correctness.
Python Solution
class Solution:
def customersWhoBoughtABNotC(self, customers: 'List[dict]', orders: 'List[dict]') -> 'List[dict]':
from collections import defaultdict
purchase_map = defaultdict(set)
for order in orders:
purchase_map[order['customer_id']].add(order['product_name'])
result = []
for customer in customers:
cid = customer['customer_id']
products = purchase_map.get(cid, set())
if 'A' in products and 'B' in products and 'C' not in products:
result.append({'customer_id': cid, 'customer_name': customer['customer_name']})
return sorted(result, key=lambda x: x['customer_id'])
Implementation Walkthrough: We first build a dictionary mapping each customer_id to a set of products they purchased. This ensures O(1) lookups for product membership. Then, we iterate over the Customers table, check the conditions for "A", "B", and "C", and collect qualifying customers. Finally, we sort the result by customer_id.
Go Solution
package main
type Customer struct {
CustomerID int
CustomerName string
}
type Order struct {
OrderID int
CustomerID int
ProductName string
}
func CustomersWhoBoughtABNotC(customers []Customer, orders []Order) []Customer {
purchaseMap := make(map[int]map[string]bool)
for _, order := range orders {
if purchaseMap[order.CustomerID] == nil {
purchaseMap[order.CustomerID] = make(map[string]bool)
}
purchaseMap[order.CustomerID][order.ProductName] = true
}
var result []Customer
for _, customer := range customers {
products, exists := purchaseMap[customer.CustomerID]
if !exists {
continue
}
if products["A"] && products["B"] && !products["C"] {
result = append(result, customer)
}
}
// Sort by CustomerID
for i := 0; i < len(result)-1; i++ {
for j := i + 1; j < len(result); j++ {
if result[i].CustomerID > result[j].CustomerID {
result[i], result[j] = result[j], result[i]
}
}
}
return result
}
Go-Specific Notes: We use a map of maps to store purchases per customer. Boolean values indicate whether a product has been purchased. Go does not have a built-in sorting for structs without implementing sort.Interface, so a simple bubble sort is used for clarity.
Worked Examples
Example Input:
Customers: [{1, "Daniel"}, {2, "Diana"}, {3, "Elizabeth"}, {4, "Jhon"}]
Orders: [
{10, 1, "A"}, {20, 1, "B"}, {30, 1, "D"}, {40, 1, "C"},
{50, 2, "A"}, {60, 3, "A"}, {70, 3, "B"}, {80, 3, "D"},
{90, 4, "C"}
]
Step Execution:
- Build
purchase_map:
| customer_id | products |
|---|---|
| 1 | {A, B, C, D} |
| 2 | {A} |
| 3 | {A, B, D} |
| 4 | {C} |
- Iterate over customers:
- Customer 1: has C → excluded
- Customer 2: missing B → excluded
- Customer 3: has A, B, no C → included
- Customer 4: missing A and B → excluded
- Sort result → Customer 3 ("Elizabeth").
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + k log k) | O(n) to scan orders, O(k log k) to sort customers where k is qualifying customers |
| Space | O(n) | O(n) for the dictionary mapping customer_id to purchased products |
The algorithm efficiently scans orders once and uses a dictionary for fast membership checking, avoiding multiple scans or joins.
Test Cases
# test cases
customers = [
{'customer_id': 1, 'customer_name': 'Daniel'},
{'customer_id': 2, 'customer_name': 'Diana'},
{'customer_id': 3, 'customer_name': 'Elizabeth'},
{'customer_id': 4, 'customer_name': 'Jhon'}
]
orders = [
{'order_id': 10, 'customer_id': 1, 'product_name': 'A'},
{'order_id': 20, 'customer_id': 1, 'product_name': 'B'},
{'order_id': 30, 'customer_id': 1, 'product_name': 'D'},
{'order_id': 40, 'customer_id': 1, 'product_name': 'C'},
{'order_id': 50, 'customer_id': 2, 'product_name': 'A'},
{'order_id': 60, 'customer_id': 3, 'product_name': 'A'},
{'order_id': 70, 'customer_id': 3, 'product_name': 'B'},
{'order_id': 80, 'customer_id': 3, 'product_name': 'D'},
{'order_id': 90, 'customer_id': 4, 'product_name': 'C'}
]
solution = Solution()
assert solution.customersWhoBoughtABNotC(customers, orders) == [{'customer_id': 3, 'customer_name': 'Elizabeth'}] # Example case
# Edge cases
assert solution.customersWhoBoughtABNotC([], []) == [] # no customers
assert solution.customersWhoBoughtABNotC(customers, []) == [] # no orders
assert solution.customersWhoBoughtABNotC(customers, [{'order_id': 100, 'customer_id': 2, 'product_name': 'B'}]) == [] # only one product each
| Test | Why |
|---|---|
| Example case | Validates typical scenario |
| No customers | Tests empty input |