Facebook Pixel

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 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),whilebobβ€²stransactionisinvalidduetoexceeding1000), while bob's transaction is invalid due to exceeding 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 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.

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

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

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

Load More