1169. Invalid Transactions
Problem Description
You need to identify invalid transactions from a list of transaction records. A transaction is considered possibly invalid if it meets either of these conditions:
- The transaction amount exceeds $1000
- The transaction occurs within 60 minutes (inclusive) of another transaction with the same person's name but in a different city
Each transaction is given as a string with comma-separated values in the format: "name,time,amount,city"
where:
name
is the person's nametime
is when the transaction occurred (in minutes)amount
is the transaction amount in dollarscity
is where the transaction took place
For example, if "Alice" makes a transaction in "NYC" at time 10, and another transaction in "LA" at time 50, both transactions are invalid because they occur within 60 minutes of each other (50 - 10 = 40 minutes β€ 60) and are in different cities.
Your task is to return a list of all transactions that are possibly invalid. The transactions can be returned in any order.
The solution uses a hash table to group transactions by name and checks each transaction against both validity conditions. It maintains a set of indices to track which transactions are invalid, avoiding duplicates. For the time-based condition, it compares each transaction with all other transactions of the same name, marking both as invalid if they occur in different cities within 60 minutes of each other.
Intuition
The key insight is that we need to check two independent conditions for each transaction, and a transaction can be invalid for either reason (or both).
For the first condition (amount > $1000), it's straightforward - we just need to check each transaction's amount individually.
For the second condition, we need to compare transactions that share the same name. This suggests grouping transactions by name would be efficient. Once grouped, we can compare each transaction within a group to find those that occur in different cities within 60 minutes of each other.
The tricky part is that when two transactions violate the time/city rule, both transactions become invalid, not just one. For example, if Alice has transactions in NYC at time 10 and LA at time 50, both transactions are marked invalid, not just the later one.
To handle this efficiently, we can:
- Group all transactions by the person's name using a hash table
- For each transaction, check if it violates the amount rule
- For each transaction, compare it with all other transactions under the same name to check the time/city rule
- Use a set to track invalid transaction indices to avoid duplicates (since a transaction might be invalid for multiple reasons or match with multiple other transactions)
The time complexity is acceptable because we only compare transactions within the same name group, not across all transactions. This grouping strategy significantly reduces the number of comparisons needed.
Learn more about Sorting patterns.
Solution Approach
We use a hash table combined with simulation to solve this problem. Here's the step-by-step implementation:
1. Data Structures Setup:
- Create a hash table
d
usingdefaultdict(list)
where each key is a person's name and the value is a list of their transactions - Create a set
idx
to store indices of invalid transactions (using a set prevents duplicates)
2. Parse and Store Transactions:
For each transaction at index i
:
- Parse the transaction string by splitting on commas:
name, time, amount, city = x.split(",")
- Convert
time
andamount
to integers - Store the transaction in the hash table as a tuple:
d[name].append((time, city, i))
- We store
(time, city, index)
for each transaction under the person's name
- We store
3. Check Invalid Conditions:
For each transaction, we check both invalidity conditions:
Condition 1 - Amount Check:
- If
amount > 1000
, add indexi
to theidx
set
Condition 2 - Time/City Check:
- Iterate through all transactions with the same name:
for t, c, j in d[name]
- For each pair, check if:
- Cities are different:
c != city
- Time difference is within 60 minutes:
abs(time - t) <= 60
- Cities are different:
- If both conditions are met, mark both transactions as invalid:
- Add current transaction index
i
toidx
- Add the compared transaction index
j
toidx
- Add current transaction index
4. Build Result:
- After processing all transactions, convert the set of invalid indices to actual transaction strings
- Return
[transactions[i] for i in idx]
to get all invalid transactions
The algorithm efficiently groups related transactions together and only compares transactions within the same name group, avoiding unnecessary comparisons across different names. The use of a set for tracking invalid indices ensures no duplicates in the final result.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with these transactions:
transactions = ["alice,20,800,mtv", "alice,50,100,beijing", "alice,51,100,mtv", "bob,55,1200,mtv"]
Step 1: Parse and Group Transactions
We create a hash table d
to group by name and a set idx
for invalid indices:
- Parse transaction 0:
"alice,20,800,mtv"
β name="alice", time=20, amount=800, city="mtv"- Add to
d["alice"] = [(20, "mtv", 0)]
- Add to
- Parse transaction 1:
"alice,50,100,beijing"
β name="alice", time=50, amount=100, city="beijing"- Add to
d["alice"] = [(20, "mtv", 0), (50, "beijing", 1)]
- Add to
- Parse transaction 2:
"alice,51,100,mtv"
β name="alice", time=51, amount=100, city="mtv"- Add to
d["alice"] = [(20, "mtv", 0), (50, "beijing", 1), (51, "mtv", 2)]
- Add to
- Parse transaction 3:
"bob,55,1200,mtv"
β name="bob", time=55, amount=1200, city="mtv"- Add to
d["bob"] = [(55, "mtv", 3)]
- Add to
Step 2: Check Each Transaction for Invalidity
Transaction 0 ("alice,20,800,mtv"
):
- Amount check: 800 β€ 1000 β (valid)
- Time/city check with other alice transactions:
- Compare with (50, "beijing", 1): |20-50| = 30 β€ 60 and "mtv" β "beijing" β Both 0 and 1 are invalid!
- Compare with (51, "mtv", 2): |20-51| = 31 β€ 60 but "mtv" = "mtv" β (same city, ok)
- Add indices 0 and 1 to
idx = {0, 1}
Transaction 1 ("alice,50,100,beijing"
):
- Amount check: 100 β€ 1000 β (valid)
- Time/city check with other alice transactions:
- Compare with (20, "mtv", 0): |50-20| = 30 β€ 60 and "beijing" β "mtv" β Both 1 and 0 are invalid!
- Compare with (51, "mtv", 2): |50-51| = 1 β€ 60 and "beijing" β "mtv" β Both 1 and 2 are invalid!
- Add indices 0, 1, 2 to
idx = {0, 1, 2}
(0 and 1 already there)
Transaction 2 ("alice,51,100,mtv"
):
- Amount check: 100 β€ 1000 β (valid)
- Time/city check with other alice transactions:
- Compare with (20, "mtv", 0): |51-20| = 31 β€ 60 but "mtv" = "mtv" β (same city, ok)
- Compare with (50, "beijing", 1): |51-50| = 1 β€ 60 and "mtv" β "beijing" β Both 2 and 1 are invalid!
- Add indices 1, 2 to
idx = {0, 1, 2}
(already there)
Transaction 3 ("bob,55,1200,mtv"
):
- Amount check: 1200 > 1000 β Invalid!
- Time/city check: Bob has no other transactions to compare
- Add index 3 to
idx = {0, 1, 2, 3}
Step 3: Build Result
Invalid indices: {0, 1, 2, 3}
Return all four transactions:
["alice,20,800,mtv", "alice,50,100,beijing", "alice,51,100,mtv", "bob,55,1200,mtv"]
Note how alice's transactions at indices 0, 1, and 2 are all invalid due to the time/city rule (even though none exceed 1000.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def invalidTransactions(self, transactions: List[str]) -> List[str]:
6 # Dictionary to store transactions grouped by name
7 # Key: name, Value: list of tuples (time, city, index)
8 transactions_by_name = defaultdict(list)
9
10 # Set to store indices of invalid transactions
11 invalid_indices = set()
12
13 # Process each transaction
14 for index, transaction_str in enumerate(transactions):
15 # Parse transaction string into components
16 name, time_str, amount_str, city = transaction_str.split(",")
17 time = int(time_str)
18 amount = int(amount_str)
19
20 # Store current transaction details grouped by name
21 transactions_by_name[name].append((time, city, index))
22
23 # Check if amount exceeds 1000 (invalid condition 1)
24 if amount > 1000:
25 invalid_indices.add(index)
26
27 # Check for conflicts with other transactions of the same person
28 # Invalid if: same name, different city, within 60 minutes
29 for prev_time, prev_city, prev_index in transactions_by_name[name]:
30 if prev_city != city and abs(time - prev_time) <= 60:
31 # Mark both transactions as invalid
32 invalid_indices.add(index)
33 invalid_indices.add(prev_index)
34
35 # Return all invalid transactions
36 return [transactions[i] for i in invalid_indices]
37
1class Solution {
2 /**
3 * Finds invalid transactions based on two criteria:
4 * 1. Transaction amount exceeds 1000
5 * 2. Transaction occurs within 60 minutes of another transaction with same name in different city
6 *
7 * @param transactions Array of transaction strings in format "name,time,amount,city"
8 * @return List of invalid transaction strings
9 */
10 public List<String> invalidTransactions(String[] transactions) {
11 // Map to store transactions grouped by person's name
12 Map<String, List<Transaction>> transactionsByName = new HashMap<>();
13
14 // Set to store indices of invalid transactions (avoids duplicates)
15 Set<Integer> invalidIndices = new HashSet<>();
16
17 // Process each transaction
18 for (int i = 0; i < transactions.length; i++) {
19 // Parse transaction string
20 String[] parts = transactions[i].split(",");
21 String name = parts[0];
22 int time = Integer.parseInt(parts[1]);
23 int amount = Integer.parseInt(parts[2]);
24 String city = parts[3];
25
26 // Add current transaction to the map
27 transactionsByName.computeIfAbsent(name, k -> new ArrayList<>())
28 .add(new Transaction(time, city, i));
29
30 // Check if transaction amount exceeds 1000
31 if (amount > 1000) {
32 invalidIndices.add(i);
33 }
34
35 // Check for conflicts with other transactions by the same person
36 for (Transaction previousTransaction : transactionsByName.get(name)) {
37 // If transaction is in different city and within 60 minutes
38 if (!city.equals(previousTransaction.city) &&
39 Math.abs(time - previousTransaction.time) <= 60) {
40 // Mark both transactions as invalid
41 invalidIndices.add(i);
42 invalidIndices.add(previousTransaction.index);
43 }
44 }
45 }
46
47 // Build result list from invalid indices
48 List<String> result = new ArrayList<>();
49 for (int invalidIndex : invalidIndices) {
50 result.add(transactions[invalidIndex]);
51 }
52
53 return result;
54 }
55}
56
57/**
58 * Helper class to store transaction details
59 */
60class Transaction {
61 int time; // Transaction time in minutes
62 String city; // City where transaction occurred
63 int index; // Original index in the transactions array
64
65 /**
66 * Constructor for Transaction
67 * @param time Transaction time
68 * @param city Transaction city
69 * @param index Original index in transactions array
70 */
71 Transaction(int time, String city, int index) {
72 this.time = time;
73 this.city = city;
74 this.index = index;
75 }
76}
77
1class Solution {
2public:
3 vector<string> invalidTransactions(vector<string>& transactions) {
4 // Map to store transactions grouped by name
5 // Each entry contains: time, city, and original index
6 unordered_map<string, vector<tuple<int, string, int>>> transactionsByName;
7
8 // Set to store indices of invalid transactions
9 unordered_set<int> invalidIndices;
10
11 // Process each transaction
12 for (int i = 0; i < transactions.size(); ++i) {
13 // Parse the transaction string
14 vector<string> parts = split(transactions[i], ',');
15 string name = parts[0];
16 int time = stoi(parts[1]);
17 int amount = stoi(parts[2]);
18 string city = parts[3];
19
20 // Store transaction data for this name
21 transactionsByName[name].push_back({time, city, i});
22
23 // Check if amount exceeds 1000
24 if (amount > 1000) {
25 invalidIndices.insert(i);
26 }
27
28 // Check for conflicts with other transactions of the same name
29 for (auto& [previousTime, previousCity, previousIndex] : transactionsByName[name]) {
30 // Invalid if: different city AND within 60 minutes
31 if (previousCity != city && abs(time - previousTime) <= 60) {
32 invalidIndices.insert(i);
33 invalidIndices.insert(previousIndex);
34 }
35 }
36 }
37
38 // Build result vector with invalid transactions
39 vector<string> result;
40 for (int index : invalidIndices) {
41 result.emplace_back(transactions[index]);
42 }
43
44 return result;
45 }
46
47private:
48 // Helper function to split a string by delimiter
49 vector<string> split(string& str, char delimiter) {
50 stringstream stringStream(str);
51 string token;
52 vector<string> tokens;
53
54 // Extract tokens separated by delimiter
55 while (getline(stringStream, token, delimiter)) {
56 tokens.emplace_back(token);
57 }
58
59 return tokens;
60 }
61};
62
1/**
2 * Identifies invalid transactions based on two criteria:
3 * 1. Amount exceeds 1000
4 * 2. Two transactions with same name occur in different cities within 60 minutes
5 * @param transactions - Array of transaction strings in format "name,time,amount,city"
6 * @returns Array of invalid transaction strings
7 */
8function invalidTransactions(transactions: string[]): string[] {
9 // Map to store transactions grouped by name
10 // Each entry contains: time, city, and original index
11 const transactionsByName: Map<string, Array<[number, string, number]>> = new Map();
12
13 // Set to store indices of invalid transactions
14 const invalidIndices: Set<number> = new Set();
15
16 // Process each transaction
17 for (let i = 0; i < transactions.length; i++) {
18 // Parse the transaction string
19 const parts = split(transactions[i], ',');
20 const name = parts[0];
21 const time = parseInt(parts[1]);
22 const amount = parseInt(parts[2]);
23 const city = parts[3];
24
25 // Initialize array for this name if it doesn't exist
26 if (!transactionsByName.has(name)) {
27 transactionsByName.set(name, []);
28 }
29
30 // Check for conflicts with other transactions of the same name
31 const existingTransactions = transactionsByName.get(name)!;
32 for (const [previousTime, previousCity, previousIndex] of existingTransactions) {
33 // Invalid if: different city AND within 60 minutes
34 if (previousCity !== city && Math.abs(time - previousTime) <= 60) {
35 invalidIndices.add(i);
36 invalidIndices.add(previousIndex);
37 }
38 }
39
40 // Store transaction data for this name
41 existingTransactions.push([time, city, i]);
42
43 // Check if amount exceeds 1000
44 if (amount > 1000) {
45 invalidIndices.add(i);
46 }
47 }
48
49 // Build result array with invalid transactions
50 const result: string[] = [];
51 for (const index of invalidIndices) {
52 result.push(transactions[index]);
53 }
54
55 return result;
56}
57
58/**
59 * Helper function to split a string by delimiter
60 * @param str - String to be split
61 * @param delimiter - Character to split by
62 * @returns Array of string tokens
63 */
64function split(str: string, delimiter: string): string[] {
65 return str.split(delimiter);
66}
67
Time and Space Complexity
Time Complexity: O(nΒ²)
The code iterates through each transaction once in the outer loop, which takes O(n)
time. For each transaction with name name
, it checks all previously processed transactions with the same name in the inner loop (for t, c, j in d[name]
). In the worst case, all n
transactions could have the same name, meaning for the i-th transaction, we check against i-1 previous transactions. This results in 1 + 2 + 3 + ... + (n-1) = n(n-1)/2
comparisons, which is O(nΒ²)
.
Space Complexity: O(n)
The space complexity consists of:
- Dictionary
d
: stores at mostn
transaction records (one entry per transaction), each containing(time, city, index)
tuples, requiringO(n)
space - Set
idx
: stores at mostn
indices (in the worst case, all transactions are invalid), requiringO(n)
space - The output list: stores at most
n
transactions, requiringO(n)
space
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Missing Bidirectional Marking for Time-Based Invalid Transactions
A critical mistake is forgetting to mark both transactions as invalid when they violate the time/city condition. Many developers incorrectly mark only the current transaction being processed, missing the fact that if transaction A makes transaction B invalid, then B also makes A invalid.
Incorrect Implementation:
# WRONG: Only marks current transaction as invalid
for prev_time, prev_city, prev_index in transactions_by_name[name]:
if prev_city != city and abs(time - prev_time) <= 60:
invalid_indices.add(index) # Only adds current index
# Missing: invalid_indices.add(prev_index)
Why This Fails: Consider these transactions:
"Alice,10,50,NYC"
"Alice,40,100,LA"
If we only mark the second transaction when comparing it with the first, we miss that the first transaction is also invalid due to the second one.
Correct Implementation:
# CORRECT: Marks both transactions as invalid
for prev_time, prev_city, prev_index in transactions_by_name[name]:
if prev_city != city and abs(time - prev_time) <= 60:
invalid_indices.add(index) # Add current transaction
invalid_indices.add(prev_index) # Add compared transaction
Additional Pitfall: Duplicate Comparisons Without Set
Another common mistake is using a list instead of a set for invalid_indices
, which can lead to duplicate entries in the result:
Incorrect:
invalid_indices = [] # Using list allows duplicates # Later... invalid_indices.append(index) # Can add same index multiple times
Correct:
invalid_indices = set() # Set automatically handles duplicates
invalid_indices.add(index) # Won't create duplicates
The set ensures each invalid transaction appears only once in the final result, even if it's marked invalid multiple times due to different conditions or multiple conflicting transactions.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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 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
Want a Structured Path to Master System Design Too? Donβt Miss This!