Facebook Pixel

2622. Cache With Time Limit

Problem Description

This problem asks you to implement a cache data structure with time-based expiration for stored key-value pairs. The cache should store integer keys and values, where each entry automatically expires after a specified duration.

The TimeLimitedCache class needs to implement three methods:

  1. set(key, value, duration): Stores or updates a key-value pair with an expiration time. The duration parameter specifies how long (in milliseconds) the entry should remain valid. The method returns true if the key already existed and was not expired before this operation, and false otherwise. If the key already exists, both its value and expiration time should be updated.

  2. get(key): Retrieves the value associated with a given key. If the key exists and hasn't expired, it returns the stored value. If the key doesn't exist or has expired, it returns -1.

  3. count(): Returns the total number of keys currently in the cache that haven't expired yet.

The key challenge is managing the expiration logic - entries should become inaccessible once their duration has elapsed, even if they're still stored in the underlying data structure. The implementation uses a hash table (Map) to store each key with its value and expiration timestamp. When accessing entries, the code checks if the current time has passed the expiration time to determine if the entry is still valid.

For example, if you call set(1, 42, 1000), the key 1 with value 42 will be accessible for 1000 milliseconds. After that time passes, calling get(1) would return -1, and the key wouldn't be counted in count().

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

Intuition

When dealing with time-based expiration, we need a way to track both the data and when it becomes invalid. The natural approach is to store the expiration timestamp alongside each value. Instead of actively removing expired entries (which would require a background process or timer), we can use a "lazy evaluation" strategy - checking if an entry is expired only when we try to access it.

The core insight is that we can calculate the expiration time when setting a value by adding the duration to the current timestamp: expiration = Date.now() + duration. This gives us an absolute point in time when the entry expires, making it easy to check validity later by comparing against the current time.

For the data structure, a hash table (Map) is the obvious choice since we need O(1) access to keys. We store each entry as a tuple [value, expirationTime], keeping both pieces of information together.

The tricky part is handling the set method's return value. We need to know if a key existed and was valid before updating it. This requires checking expiration status before overwriting the entry. Even if a key exists in the map, it might be expired, so we treat expired keys as if they don't exist.

For counting valid keys, we must iterate through all entries and filter out expired ones. While this is O(n), there's no way around checking each entry's expiration status without maintaining additional data structures or using active cleanup, which would complicate the implementation.

The solution elegantly handles expiration checking through a helper method #isExpired that encapsulates the logic of verifying both key existence and comparing timestamps, making the main methods cleaner and avoiding code duplication.

Solution Approach

The solution uses a hash table (Map) to store key-value pairs along with their expiration timestamps. Here's how each component works:

Data Structure:

  • A private Map #cache stores entries as Map<number, [value: number, expire: number]>
  • Each entry contains a tuple with the value and its expiration timestamp

Helper Method - #isExpired:

#isExpired = (key: number) =>
    this.#cache.has(key) &&
    (this.#cache.get(key)?.[1] ?? Number.NEGATIVE_INFINITY) < Date.now();

This method checks if a key exists and if its expiration time has passed. It returns true only if the key exists in the map AND the current time exceeds the stored expiration time.

set Method Implementation:

  1. First, check if the key exists and is not expired to determine the return value
  2. Store the new entry with expiration time calculated as Date.now() + duration
  3. Return true if the key previously existed (regardless of update), false otherwise
set(key: number, value: number, duration: number): boolean {
    const isExist = this.#cache.has(key) && !this.#isExpired(key);
    this.#cache.set(key, [value, Date.now() + duration]);
    return isExist;
}

get Method Implementation:

  1. Check if the key is expired using the helper method
  2. If expired, return -1
  3. Otherwise, retrieve the value from the tuple stored in the map
get(key: number): number {
    if (this.#isExpired(key)) return -1;
    const res = this.#cache.get(key)?.[0] ?? -1;
    return res;
}

count Method Implementation:

  1. Convert the Map to an array of entries
  2. Filter out expired keys by checking each one
  3. Return the length of the filtered array
count(): number {
    const xs = Array.from(this.#cache).filter(([key]) => !this.#isExpired(key));
    return xs.length;
}

The approach leverages TypeScript's private fields (denoted by #) for encapsulation and uses optional chaining (?.) and nullish coalescing (??) operators for safe property access. The solution avoids active cleanup of expired entries, instead checking expiration status on-demand, which simplifies the implementation while maintaining correct behavior.

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 simple example to illustrate how the TimeLimitedCache works:

const cache = new TimeLimitedCache();

// Time T=0ms: Set key 1 with value 10, expires in 50ms
cache.set(1, 10, 50);  // returns false (key didn't exist before)
// Cache state: {1: [10, T+50ms]}

// Time T=20ms: Set key 2 with value 20, expires in 30ms (at T+50ms)
cache.set(2, 20, 30);  // returns false
// Cache state: {1: [10, T+50ms], 2: [20, T+50ms]}

// Time T=25ms: Get key 1
cache.get(1);  // returns 10 (not expired yet, T+25 < T+50)

// Time T=30ms: Count active keys
cache.count();  // returns 2 (both keys still valid)

// Time T=40ms: Update key 1 with new value 15, expires in 40ms (at T+80ms)
cache.set(1, 15, 40);  // returns true (key 1 existed and wasn't expired)
// Cache state: {1: [15, T+80ms], 2: [20, T+50ms]}

// Time T=55ms: Try to get key 2
cache.get(2);  // returns -1 (expired at T+50ms)

// Time T=60ms: Count active keys
cache.count();  // returns 1 (only key 1 is still valid)

// Time T=85ms: Try to get key 1
cache.get(1);  // returns -1 (expired at T+80ms)

Step-by-step breakdown:

  1. Initial set: When we first add key 1, the cache stores [10, currentTime + 50]. Since the key didn't exist, it returns false.

  2. Checking expiration: When we call get(1) at T=25ms, the #isExpired helper checks if T+50 < T+25, which is false, so the key is still valid and returns 10.

  3. Updating existing key: At T=40ms, when we call set(1, 15, 40), the method first checks if key 1 exists and isn't expired (true), then updates both the value and expiration time to [15, T+80], returning true.

  4. Expired access: At T=55ms, key 2 has expired (T+50 < T+55), so get(2) returns -1 even though the entry still exists in the Map.

  5. Counting valid keys: The count() method iterates through all entries and filters out expired ones using the #isExpired helper, returning only the count of valid keys.

Solution Implementation

1# Map to store cache entries with their values and expiration timestamps
2# Key: cache key, Value: tuple of (cached value, expiration timestamp in milliseconds)
3cache = {}
4
5def is_expired(key):
6    """
7    Checks if a cache entry has expired
8  
9    Args:
10        key: The cache key to check
11      
12    Returns:
13        True if the entry exists and has expired, False otherwise
14    """
15    # Check if key exists in cache
16    if key not in cache:
17        return False
18  
19    # Get expiration timestamp and compare with current time
20    import time
21    expiration_time = cache[key][1]
22    return expiration_time < time.time() * 1000
23
24def set(key, value, duration):
25    """
26    Sets a key-value pair in the cache with a time limit
27  
28    Args:
29        key: The cache key
30        value: The value to cache
31        duration: Time in milliseconds until expiration
32      
33    Returns:
34        True if the key already existed (and was not expired), False otherwise
35    """
36    import time
37  
38    # Check if key exists and is not expired before updating
39    exists_and_not_expired = key in cache and not is_expired(key)
40  
41    # Calculate expiration timestamp
42    expiration_time = time.time() * 1000 + duration
43  
44    # Store or update the cache entry
45    cache[key] = (value, expiration_time)
46  
47    return exists_and_not_expired
48
49def get(key):
50    """
51    Retrieves a value from the cache
52  
53    Args:
54        key: The cache key to retrieve
55      
56    Returns:
57        The cached value if it exists and hasn't expired, -1 otherwise
58    """
59    # Return -1 if key doesn't exist or has expired
60    if key not in cache or is_expired(key):
61        return -1
62  
63    # Retrieve and return the cached value
64    cached_value = cache[key][0]
65    return cached_value
66
67def count():
68    """
69    Counts the number of non-expired entries in the cache
70  
71    Returns:
72        The count of valid (non-expired) cache entries
73    """
74    # Filter cache entries to count only non-expired ones
75    valid_entries_count = 0
76  
77    for key in cache:
78        if not is_expired(key):
79            valid_entries_count += 1
80  
81    return valid_entries_count
82
1import java.util.HashMap;
2import java.util.Map;
3
4public class TimeLimitedCache {
5    // Map to store cache entries with their values and expiration timestamps
6    // Key: cache key, Value: array containing [cached value, expiration timestamp in milliseconds]
7    private final Map<Integer, long[]> cache = new HashMap<>();
8  
9    /**
10     * Checks if a cache entry has expired
11     * @param key - The cache key to check
12     * @return true if the entry exists and has expired, false otherwise
13     */
14    private boolean isExpired(int key) {
15        // Check if key exists in cache
16        if (!cache.containsKey(key)) {
17            return false;
18        }
19      
20        // Get expiration timestamp and compare with current time
21        long[] entry = cache.get(key);
22        long expirationTime = entry != null ? entry[1] : Long.MIN_VALUE;
23        return expirationTime < System.currentTimeMillis();
24    }
25  
26    /**
27     * Sets a key-value pair in the cache with a time limit
28     * @param key - The cache key
29     * @param value - The value to cache
30     * @param duration - Time in milliseconds until expiration
31     * @return true if the key already existed (and was not expired), false otherwise
32     */
33    public boolean set(int key, int value, long duration) {
34        // Check if key exists and is not expired before updating
35        boolean existsAndNotExpired = cache.containsKey(key) && !isExpired(key);
36      
37        // Calculate expiration timestamp
38        long expirationTime = System.currentTimeMillis() + duration;
39      
40        // Store or update the cache entry
41        cache.put(key, new long[]{value, expirationTime});
42      
43        return existsAndNotExpired;
44    }
45  
46    /**
47     * Retrieves a value from the cache
48     * @param key - The cache key to retrieve
49     * @return The cached value if it exists and hasn't expired, -1 otherwise
50     */
51    public int get(int key) {
52        // Return -1 if key doesn't exist or has expired
53        if (!cache.containsKey(key) || isExpired(key)) {
54            return -1;
55        }
56      
57        // Retrieve and return the cached value
58        long[] entry = cache.get(key);
59        int cachedValue = entry != null ? (int) entry[0] : -1;
60        return cachedValue;
61    }
62  
63    /**
64     * Counts the number of non-expired entries in the cache
65     * @return The count of valid (non-expired) cache entries
66     */
67    public int count() {
68        // Filter cache entries to count only non-expired ones
69        int validEntriesCount = 0;
70      
71        for (Integer key : cache.keySet()) {
72            if (!isExpired(key)) {
73                validEntriesCount++;
74            }
75        }
76      
77        return validEntriesCount;
78    }
79}
80
1#include <unordered_map>
2#include <chrono>
3#include <utility>
4
5// Map to store cache entries with their values and expiration timestamps
6// Key: cache key, Value: pair of [cached value, expiration timestamp in milliseconds]
7std::unordered_map<int, std::pair<int, long long>> cache;
8
9/**
10 * Gets the current time in milliseconds since epoch
11 * @return Current timestamp in milliseconds
12 */
13long long getCurrentTimeMs() {
14    return std::chrono::duration_cast<std::chrono::milliseconds>(
15        std::chrono::system_clock::now().time_since_epoch()
16    ).count();
17}
18
19/**
20 * Checks if a cache entry has expired
21 * @param key - The cache key to check
22 * @return true if the entry exists and has expired, false otherwise
23 */
24bool isExpired(int key) {
25    // Check if key exists in cache
26    auto it = cache.find(key);
27    if (it == cache.end()) {
28        return false;
29    }
30  
31    // Get expiration timestamp and compare with current time
32    long long expirationTime = it->second.second;
33    return expirationTime < getCurrentTimeMs();
34}
35
36/**
37 * Sets a key-value pair in the cache with a time limit
38 * @param key - The cache key
39 * @param value - The value to cache
40 * @param duration - Time in milliseconds until expiration
41 * @return true if the key already existed (and was not expired), false otherwise
42 */
43bool set(int key, int value, int duration) {
44    // Check if key exists and is not expired before updating
45    bool existsAndNotExpired = (cache.find(key) != cache.end()) && !isExpired(key);
46  
47    // Calculate expiration timestamp
48    long long expirationTime = getCurrentTimeMs() + duration;
49  
50    // Store or update the cache entry
51    cache[key] = std::make_pair(value, expirationTime);
52  
53    return existsAndNotExpired;
54}
55
56/**
57 * Retrieves a value from the cache
58 * @param key - The cache key to retrieve
59 * @return The cached value if it exists and hasn't expired, -1 otherwise
60 */
61int get(int key) {
62    // Return -1 if key doesn't exist
63    auto it = cache.find(key);
64    if (it == cache.end() || isExpired(key)) {
65        return -1;
66    }
67  
68    // Retrieve and return the cached value
69    return it->second.first;
70}
71
72/**
73 * Counts the number of non-expired entries in the cache
74 * @return The count of valid (non-expired) cache entries
75 */
76int count() {
77    // Filter cache entries to count only non-expired ones
78    int validEntriesCount = 0;
79  
80    for (const auto& entry : cache) {
81        if (!isExpired(entry.first)) {
82            validEntriesCount++;
83        }
84    }
85  
86    return validEntriesCount;
87}
88
1// Map to store cache entries with their values and expiration timestamps
2// Key: cache key, Value: tuple of [cached value, expiration timestamp in milliseconds]
3const cache: Map<number, [value: number, expire: number]> = new Map();
4
5/**
6 * Checks if a cache entry has expired
7 * @param key - The cache key to check
8 * @returns true if the entry exists and has expired, false otherwise
9 */
10const isExpired = (key: number): boolean => {
11    // Check if key exists in cache
12    if (!cache.has(key)) {
13        return false;
14    }
15  
16    // Get expiration timestamp and compare with current time
17    const expirationTime = cache.get(key)?.[1] ?? Number.NEGATIVE_INFINITY;
18    return expirationTime < Date.now();
19};
20
21/**
22 * Sets a key-value pair in the cache with a time limit
23 * @param key - The cache key
24 * @param value - The value to cache
25 * @param duration - Time in milliseconds until expiration
26 * @returns true if the key already existed (and was not expired), false otherwise
27 */
28function set(key: number, value: number, duration: number): boolean {
29    // Check if key exists and is not expired before updating
30    const existsAndNotExpired = cache.has(key) && !isExpired(key);
31  
32    // Calculate expiration timestamp
33    const expirationTime = Date.now() + duration;
34  
35    // Store or update the cache entry
36    cache.set(key, [value, expirationTime]);
37  
38    return existsAndNotExpired;
39}
40
41/**
42 * Retrieves a value from the cache
43 * @param key - The cache key to retrieve
44 * @returns The cached value if it exists and hasn't expired, -1 otherwise
45 */
46function get(key: number): number {
47    // Return -1 if key doesn't exist or has expired
48    if (!cache.has(key) || isExpired(key)) {
49        return -1;
50    }
51  
52    // Retrieve and return the cached value
53    const cachedValue = cache.get(key)?.[0] ?? -1;
54    return cachedValue;
55}
56
57/**
58 * Counts the number of non-expired entries in the cache
59 * @returns The count of valid (non-expired) cache entries
60 */
61function count(): number {
62    // Filter cache entries to count only non-expired ones
63    let validEntriesCount = 0;
64  
65    for (const [key] of cache) {
66        if (!isExpired(key)) {
67            validEntriesCount++;
68        }
69    }
70  
71    return validEntriesCount;
72}
73

Time and Space Complexity

Time Complexity:

  • set(key, value, duration): O(1) - The method performs hash map operations (has, get, set) which are constant time operations on average.
  • get(key): O(1) - The method performs hash map operations (has, get) which are constant time operations on average.
  • count(): O(n) - The method iterates through all entries in the cache using Array.from() and filter(), where n is the number of entries in the cache.

Space Complexity:

  • Overall space: O(n) - The cache stores up to n key-value pairs along with their expiration times.
  • set(): O(1) - Only stores or updates a single entry.
  • get(): O(1) - No additional space is used beyond temporary variables.
  • count(): O(n) - Creates a new array from the cache entries during the filtering operation.

Note: The implementation has a bug in the set method. The condition !this.#isExpired(key) should likely be reversed or removed entirely, as it prevents setting values when the key doesn't exist or is expired. The current logic only sets a value if the key exists and is NOT expired, which contradicts the expected behavior of always setting the value regardless of expiration status.

Common Pitfalls

1. Time Unit Mismatch Between time.time() and Duration Parameter

The most critical pitfall in this implementation is the inconsistent handling of time units. Python's time.time() returns the current time in seconds since the epoch, but the problem specification requires the duration parameter to be in milliseconds.

The Bug:

def set(key, value, duration):
    import time
    # time.time() returns seconds, but duration is in milliseconds
    expiration_time = time.time() * 1000 + duration  # Mixing units incorrectly

While the code multiplies time.time() by 1000 to convert to milliseconds, this creates precision issues due to floating-point arithmetic and makes the code error-prone.

The Solution: Use time.time_ns() for nanosecond precision or consistently work with milliseconds:

import time

def get_current_time_ms():
    """Returns current time in milliseconds with proper precision"""
    return int(time.time() * 1000)

def set(key, value, duration):
    exists_and_not_expired = key in cache and not is_expired(key)
    expiration_time = get_current_time_ms() + duration
    cache[key] = (value, expiration_time)
    return exists_and_not_expired

def is_expired(key):
    if key not in cache:
        return False
    expiration_time = cache[key][1]
    return expiration_time < get_current_time_ms()

2. Global State Management

Using a global cache dictionary can cause issues in concurrent environments or when multiple cache instances are needed.

The Problem:

cache = {}  # Global variable - shared across all usages

The Solution: Encapsulate the cache in a class:

class TimeLimitedCache:
    def __init__(self):
        self.cache = {}
  
    def set(self, key, value, duration):
        exists_and_not_expired = key in self.cache and not self._is_expired(key)
        expiration_time = self._get_current_time_ms() + duration
        self.cache[key] = (value, expiration_time)
        return exists_and_not_expired
  
    def get(self, key):
        if key not in self.cache or self._is_expired(key):
            return -1
        return self.cache[key][0]
  
    def count(self):
        return sum(1 for key in self.cache if not self._is_expired(key))
  
    def _is_expired(self, key):
        if key not in self.cache:
            return False
        return self.cache[key][1] < self._get_current_time_ms()
  
    def _get_current_time_ms(self):
        import time
        return int(time.time() * 1000)

3. Memory Leak from Never Cleaning Expired Entries

The current implementation never removes expired entries from the dictionary, leading to unbounded memory growth.

The Solution: Add periodic cleanup or clean on access:

def get(key):
    if key not in cache:
        return -1
  
    if is_expired(key):
        # Clean up expired entry when detected
        del cache[key]
        return -1
  
    return cache[key][0]

def count():
    # Clean up expired entries during count
    expired_keys = [key for key in cache if is_expired(key)]
    for key in expired_keys:
        del cache[key]
  
    return len(cache)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

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

Load More