title: Alert Using Same Key-Card Three or More Times in a One Hour Period
slug: alert-using-same-key
difficulty: medium
leetcode_id: 1604
leetcode_url: https://leetcode.com/problems/alert-using-same-key-card-three-or-more-times-in-a-one-hour-period/
categories:
  - arrays
  - hash-tables
  - strings
  - sorting
patterns:
  - sliding-window

description: |
  LeetCode company workers use key-cards to unlock office doors. Each time a worker uses their key-card, the security system saves the worker's name and the time when it was used. The system emits an **alert** if any worker uses the key-card **three or more times** in a one-hour period.

  You are given a list of strings `keyName` and `keyTime` where `[keyName[i], keyTime[i]]` corresponds to a person's name and the time when their key-card was used **in a single day**.

  Access times are given in the **24-hour time format "HH:MM"**, such as `"23:51"` and `"09:49"`.

  Return *a list of unique worker names who received an alert for frequent keycard use*. Sort the names in **ascending order alphabetically**.

  Notice that `"10:00"` - `"11:00"` is considered to be within a one-hour period, while `"22:51"` - `"23:52"` is not considered to be within a one-hour period.

constraints: |
  - `1 <= keyName.length, keyTime.length <= 10^5`
  - `keyName.length == keyTime.length`
  - `keyTime[i]` is in the format **"HH:MM"**
  - `[keyName[i], keyTime[i]]` is **unique**
  - `1 <= keyName[i].length <= 10`
  - `keyName[i]` contains only lowercase English letters

examples:
  - input: 'keyName = ["daniel","daniel","daniel","luis","luis","luis","luis"], keyTime = ["10:00","10:40","11:00","09:00","11:00","13:00","15:00"]'
    output: '["daniel"]'
    explanation: '"daniel" used the keycard 3 times in a one-hour period ("10:00", "10:40", "11:00").'
  - input: 'keyName = ["alice","alice","alice","bob","bob","bob","bob"], keyTime = ["12:01","12:00","18:00","21:00","21:20","21:30","23:00"]'
    output: '["bob"]'
    explanation: '"bob" used the keycard 3 times in a one-hour period ("21:00", "21:20", "21:30").'

explanation:
  intuition: |
    Imagine you're a security guard monitoring a list of badge swipes throughout the day. Your task is to flag anyone who swiped their badge three or more times within any 60-minute window.

    The key insight is that checking *every possible* one-hour window for each person would be inefficient. Instead, if we **sort each person's access times chronologically**, we can use a clever observation: for any three consecutive swipes, if the 1<sup>st</sup> and 3<sup>rd</sup> are within 60 minutes of each other, then all three are within a one-hour period.

    Think of it like this: if you line up someone's swipes in time order and check every group of three consecutive swipes, you only need to verify that the earliest and latest in each group are at most 60 minutes apart. This works because sorting guarantees the middle swipe is between them.

    This transforms a potentially complex sliding window problem into a simple linear scan after sorting.

  approach: |
    We solve this using a **Hash Map + Sorting** approach:

    **Step 1: Group access times by person**

    - Create a hash map where each key is a worker's name
    - Each value is a list of their access times converted to minutes (for easy comparison)
    - Convert "HH:MM" to total minutes: `hours * 60 + minutes`

    &nbsp;

    **Step 2: Sort each person's access times**

    - For each worker, sort their list of access times in ascending order
    - Sorting enables the consecutive-triplet checking strategy

    &nbsp;

    **Step 3: Check consecutive triplets**

    - For each worker with 3 or more swipes, iterate through their sorted times
    - For each index `i` from `0` to `n-3`, check if `times[i+2] - times[i] <= 60`
    - If any triplet satisfies this condition, add the worker to the alert list

    &nbsp;

    **Step 4: Return sorted result**

    - Sort the list of flagged workers alphabetically
    - Return the result

  common_pitfalls:
    - title: Not Sorting Access Times
      description: |
        Without sorting, you cannot use the consecutive-triplet approach. You'd need to check all possible combinations of three swipes, leading to O(n^3) per person.

        For example, if Alice's swipes are recorded as `["12:01", "12:00", "18:00"]`, checking consecutive elements without sorting would miss that `"12:00"` and `"12:01"` are adjacent in time.
      wrong_approach: "Checking triplets in the original unsorted order"
      correct_approach: "Sort times first, then check consecutive triplets"

    - title: Incorrect One-Hour Window Definition
      description: |
        The problem states `"10:00"` to `"11:00"` is within one hour (inclusive), meaning the difference should be `<= 60` minutes, not `< 60`.

        However, `"22:51"` to `"23:52"` (61 minutes apart) is NOT within one hour. Be careful with the boundary condition.
      wrong_approach: "Using strict less than (< 60)"
      correct_approach: "Use less than or equal (<= 60)"

    - title: Forgetting to Handle Multiple Days
      description: |
        The problem specifies all times are within a **single day**, so there's no midnight wraparound to handle. If you try to account for times spanning midnight (like `"23:30"` to `"00:30"`), you'll introduce bugs.

        Trust the problem constraints and keep the logic simple.
      wrong_approach: "Adding complex midnight wraparound logic"
      correct_approach: "Treat all times as within a single day (no wraparound)"

    - title: Time Conversion Errors
      description: |
        When converting `"HH:MM"` to minutes, ensure you parse the string correctly. A common mistake is treating `"09:05"` incorrectly or forgetting to multiply hours by 60.

        The correct formula is: `int(time[:2]) * 60 + int(time[3:])` or split by `":"` and convert.
      wrong_approach: "Incorrect string parsing"
      correct_approach: "Split by colon and compute hours * 60 + minutes"

  key_takeaways:
    - "**Grouping + sorting pattern**: When analysing events per entity over time, group by entity and sort by timestamp first"
    - "**Consecutive element insight**: After sorting, checking windows of size `k` only requires comparing element `i` with element `i+k-1`"
    - "**Time conversion simplifies comparison**: Converting `HH:MM` to total minutes makes arithmetic comparison straightforward"
    - "**This pattern applies to**: Log analysis, session tracking, rate limiting, and any problem involving time-based event grouping"

  time_complexity: "O(n log n). We iterate through all `n` entries once to group them, then sort each person's times. In the worst case (one person with all entries), sorting dominates at O(n log n)."
  space_complexity: "O(n). We store all access times in the hash map, and the result list can contain up to O(n) unique names in the worst case."

solutions:
  - approach_name: Hash Map with Sorting
    is_optimal: true
    code: |
      def alertNames(keyName: list[str], keyTime: list[str]) -> list[str]:
          from collections import defaultdict

          # Group access times by person, converting to minutes
          access_times = defaultdict(list)
          for name, time in zip(keyName, keyTime):
              # Convert "HH:MM" to total minutes for easy comparison
              hours, minutes = int(time[:2]), int(time[3:])
              access_times[name].append(hours * 60 + minutes)

          result = []

          for name, times in access_times.items():
              # Sort times chronologically
              times.sort()

              # Check if any 3 consecutive swipes are within 60 minutes
              for i in range(len(times) - 2):
                  # If the gap between 1st and 3rd is <= 60, all 3 are in one hour
                  if times[i + 2] - times[i] <= 60:
                      result.append(name)
                      break  # No need to check further for this person

          # Return names sorted alphabetically
          return sorted(result)
    explanation: |
      **Time Complexity:** O(n log n) — Grouping is O(n), sorting all times is O(n log n) in aggregate, and checking triplets is O(n).

      **Space Complexity:** O(n) — Hash map stores all access times.

      We group by name, sort each person's times, and check consecutive triplets. The key insight is that after sorting, if `times[i+2] - times[i] <= 60`, then the three swipes at indices `i`, `i+1`, and `i+2` all occurred within a one-hour window.

  - approach_name: Brute Force (Check All Triplets)
    is_optimal: false
    code: |
      def alertNames(keyName: list[str], keyTime: list[str]) -> list[str]:
          from collections import defaultdict

          # Group access times by person
          access_times = defaultdict(list)
          for name, time in zip(keyName, keyTime):
              hours, minutes = int(time[:2]), int(time[3:])
              access_times[name].append(hours * 60 + minutes)

          result = []

          for name, times in access_times.items():
              n = len(times)
              found = False

              # Check all combinations of 3 times
              for i in range(n):
                  if found:
                      break
                  for j in range(i + 1, n):
                      if found:
                          break
                      for k in range(j + 1, n):
                          # Check if all three are within 60 minutes
                          max_time = max(times[i], times[j], times[k])
                          min_time = min(times[i], times[j], times[k])
                          if max_time - min_time <= 60:
                              result.append(name)
                              found = True
                              break

          return sorted(result)
    explanation: |
      **Time Complexity:** O(n^3) — For each person, we check all combinations of 3 times.

      **Space Complexity:** O(n) — Hash map stores all access times.

      This brute force approach checks every possible triplet of access times for each person. While correct, it's extremely slow for large inputs. With `n = 10^5` entries for a single person, this would require checking up to 10^15 combinations — far too slow. This illustrates why the sorted consecutive-triplet approach is necessary.
