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:
-
set(key, value, duration)
: Stores or updates a key-value pair with an expiration time. Theduration
parameter specifies how long (in milliseconds) the entry should remain valid. The method returnstrue
if the key already existed and was not expired before this operation, andfalse
otherwise. If the key already exists, both its value and expiration time should be updated. -
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
. -
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()
.
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 asMap<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:
- First, check if the key exists and is not expired to determine the return value
- Store the new entry with expiration time calculated as
Date.now() + duration
- 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:
- Check if the key is expired using the helper method
- If expired, return
-1
- 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:
- Convert the Map to an array of entries
- Filter out expired keys by checking each one
- 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 EvaluatorExample 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:
-
Initial set: When we first add key 1, the cache stores
[10, currentTime + 50]
. Since the key didn't exist, it returnsfalse
. -
Checking expiration: When we call
get(1)
at T=25ms, the#isExpired
helper checks ifT+50 < T+25
, which is false, so the key is still valid and returns 10. -
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]
, returningtrue
. -
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. -
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 usingArray.from()
andfilter()
, wheren
is the number of entries in the cache.
Space Complexity:
- Overall space:
O(n)
- The cache stores up ton
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)
How does merge sort divide the problem into subproblems?
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!