1169. Invalid Transactions

MediumArrayHash TableStringSorting
Leetcode Link

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:

  1. The transaction amount exceeds $1000
  2. 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 name
  • time is when the transaction occurred (in minutes)
  • amount is the transaction amount in dollars
  • city 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Group all transactions by the person's name using a hash table
  2. For each transaction, check if it violates the amount rule
  3. For each transaction, compare it with all other transactions under the same name to check the time/city rule
  4. 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 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

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 using defaultdict(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 and amount 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

3. Check Invalid Conditions:

For each transaction, we check both invalidity conditions:

Condition 1 - Amount Check:

  • If amount > 1000, add index i to the idx 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
  • If both conditions are met, mark both transactions as invalid:
    • Add current transaction index i to idx
    • Add the compared transaction index j to idx

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 Evaluator

Example 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)]
  • 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)]
  • 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)]
  • Parse transaction 3: "bob,55,1200,mtv" → name="bob", time=55, amount=1200, city="mtv"
    • Add to d["bob"] = [(55, "mtv", 3)]

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),whilebobstransactionisinvalidduetoexceeding1000), while bob's transaction is invalid due to exceeding 1000.

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 most n transaction records (one entry per transaction), each containing (time, city, index) tuples, requiring O(n) space
  • Set idx: stores at most n indices (in the worst case, all transactions are invalid), requiring O(n) space
  • The output list: stores at most n transactions, requiring O(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.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More