2805. Custom Interval 🔒
Problem Description
This problem asks you to implement two custom functions that manage intervals with a variable timing pattern.
Function 1: customInterval
You need to create a function that takes three parameters:
fn
: A function to be executed repeatedlydelay
: The initial delay before the first execution (in milliseconds)period
: The increment value for calculating subsequent delays (in milliseconds)
The function should execute fn
at intervals following a linear pattern. The timing for each execution is calculated using the formula: delay + period * count
, where count
starts at 0 and increases by 1 after each execution.
For example, if delay = 100
and period = 50
:
- First execution happens after:
100 + 50 * 0 = 100ms
- Second execution happens after:
100 + 50 * 1 = 150ms
- Third execution happens after:
100 + 50 * 2 = 200ms
- And so on...
The function should return a unique id
(number) that can be used to stop the interval later.
Function 2: customClearInterval
You need to create a function that takes one parameter:
id
: The identifier returned bycustomInterval
This function should stop the execution of the interval associated with the given id
, preventing any future executions of the function fn
.
The implementation uses recursive setTimeout
calls rather than setInterval
to achieve the variable timing pattern. A Map is used to track active intervals by their IDs, allowing them to be cleared when needed.
Intuition
The key insight is that JavaScript's built-in setInterval
function executes at fixed intervals, but we need variable intervals that increase linearly. Since each execution happens at a different delay (delay + period * count
), we can't use setInterval
directly.
Instead of trying to modify setInterval
, we can use setTimeout
recursively. Each time the function executes, we schedule the next execution with a new calculated delay. This gives us full control over when each execution happens.
Why recursive setTimeout
works:
- After each execution, we calculate the next delay based on the current
count
- We schedule the next execution using
setTimeout
with this new delay - This creates a chain of scheduled executions, each with its own timing
The challenge is managing these recursive timeouts so they can be cancelled. We need to:
- Track the current active timeout for each custom interval
- Update this reference each time we create a new timeout in the chain
- Be able to stop the entire chain by clearing the current timeout
A Map is perfect for this because:
- We can use unique IDs as keys to track multiple custom intervals simultaneously
- We can easily update the timeout reference for a given ID
- We can check if an interval exists and clear it when needed
The Date.now()
approach for generating IDs ensures uniqueness since it returns the current timestamp in milliseconds. While there's a theoretical chance of collision if two intervals are created at the exact same millisecond, it's practical enough for most use cases.
The recursive pattern naturally handles the incrementing count
- each recursive call represents the next iteration, so we increment count
after each execution and use it to calculate the delay for the next timeout.
Solution Approach
The solution uses a Map data structure to track active intervals and recursive setTimeout
calls to achieve the variable timing pattern.
Data Structure:
intervalMap
: A Map that stores the relationship between interval IDs and their current timeout objects- Key: The unique ID (number) for each custom interval
- Value: The NodeJS.Timeout object that can be cleared
Implementation of customInterval
:
-
Initialize the counter: Start with
count = 0
to track how many times the function has executed -
Create a recursive function: Define
recursiveTimeout
that:- Schedules the next execution using
setTimeout
- Calculates the delay as
delay + period * count
- Inside the timeout callback:
- Executes the provided function
fn()
- Increments
count++
for the next iteration - Calls itself recursively to schedule the next execution
- Executes the provided function
- Stores the timeout reference in the Map with the interval's ID
- Schedules the next execution using
-
Generate unique ID: Use
Date.now()
to create a timestamp-based unique identifier -
Start the chain: Call
recursiveTimeout()
to begin the interval sequence -
Return the ID: Return the generated ID for later reference
Implementation of customClearInterval
:
-
Check existence: Verify if the provided ID exists in the
intervalMap
-
Clear the timeout: If it exists:
- Retrieve the timeout object using
intervalMap.get(id)
- Call
clearTimeout()
to cancel the scheduled execution - This breaks the recursive chain since the next
recursiveTimeout
won't be called
- Retrieve the timeout object using
-
Clean up: Remove the entry from the Map using
intervalMap.delete(id)
Key Pattern - Recursive setTimeout:
The core pattern replaces traditional setInterval
with a self-scheduling timeout:
function recursiveTimeout() {
setTimeout(() => {
// Execute task
// Update state
recursiveTimeout(); // Schedule next execution
}, calculatedDelay);
}
This pattern allows dynamic delay calculation for each iteration, enabling the linear progression of intervals required by the problem.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example where we want to log "Hello" with increasing delays.
Setup:
fn = () => console.log("Hello")
delay = 100ms
period = 50ms
Step 1: Call customInterval
const id = customInterval(() => console.log("Hello"), 100, 50);
// Returns id = 1699564800000 (example timestamp)
Step 2: First execution (count = 0)
- Calculate delay:
100 + 50 * 0 = 100ms
- Schedule setTimeout for 100ms
- After 100ms:
- Execute:
console.log("Hello")
→ Output: "Hello" - Increment count to 1
- Schedule next timeout
- Execute:
Step 3: Second execution (count = 1)
- Calculate delay:
100 + 50 * 1 = 150ms
- Schedule setTimeout for 150ms
- After 150ms:
- Execute:
console.log("Hello")
→ Output: "Hello" - Increment count to 2
- Update intervalMap[1699564800000] with new timeout reference
- Schedule next timeout
- Execute:
Step 4: Third execution (count = 2)
- Calculate delay:
100 + 50 * 2 = 200ms
- Schedule setTimeout for 200ms
- After 200ms:
- Execute:
console.log("Hello")
→ Output: "Hello" - Increment count to 3
- Update intervalMap[1699564800000] with new timeout reference
- Schedule next timeout
- Execute:
Step 5: Clear the interval
customClearInterval(1699564800000);
- Find timeout in intervalMap using id 1699564800000
- Call clearTimeout on the current timeout object
- Remove entry from intervalMap
- The recursive chain breaks - no more executions
Timeline visualization:
Time: 0ms ---100ms---250ms---450ms---[CLEARED] Event: Start Hello Hello Hello Stop Delay: 100ms 150ms 200ms
Notice how each delay increases by 50ms (the period), creating the pattern: 100ms, 150ms, 200ms, 250ms, etc. The total time from start to each execution follows the cumulative pattern.
Solution Implementation
1import threading
2import time
3from typing import Callable, Dict
4
5# Global dictionary to store active timer objects for each custom interval
6interval_map: Dict[int, threading.Timer] = {}
7
8def custom_interval(fn: Callable, delay: float, period: float) -> int:
9 """
10 Creates a custom interval that executes a function repeatedly
11
12 Args:
13 fn: The function to execute at each interval
14 delay: Initial delay before the first execution (in seconds)
15 period: Time between subsequent executions (in seconds)
16
17 Returns:
18 A unique identifier for this interval
19 """
20 # Generate unique ID for this interval using timestamp
21 interval_id = int(time.time() * 1000000)
22
23 # Counter to track the number of executions
24 execution_count = [0] # Use list to maintain reference in closure
25
26 def schedule_next_execution():
27 """
28 Recursive function that schedules the next timeout
29 Each execution schedules the next one with calculated delay
30 """
31 # Calculate the total delay for the current execution
32 current_delay = delay + period * execution_count[0]
33
34 def execute_and_reschedule():
35 """
36 Inner function to execute the provided function and schedule next execution
37 """
38 # Execute the provided function
39 fn()
40
41 # Increment the execution counter
42 execution_count[0] += 1
43
44 # Schedule the next execution
45 schedule_next_execution()
46
47 # Create timer and store its reference in the dictionary
48 timer = threading.Timer(current_delay, execute_and_reschedule)
49 timer.daemon = True # Make timer thread daemon to not block program exit
50 timer.start()
51
52 # Store the timer object in the dictionary for later cancellation
53 interval_map[interval_id] = timer
54
55 # Start the first execution
56 schedule_next_execution()
57
58 return interval_id
59
60
61def custom_clear_interval(id: int) -> None:
62 """
63 Clears a custom interval created by custom_interval
64
65 Args:
66 id: The unique identifier of the interval to clear
67 """
68 # Check if the interval exists in the dictionary
69 if id in interval_map:
70 # Cancel the timer associated with this interval
71 timer = interval_map[id]
72 timer.cancel()
73
74 # Remove the interval from the dictionary
75 del interval_map[id]
76
1import java.util.Map;
2import java.util.concurrent.ConcurrentHashMap;
3import java.util.concurrent.Executors;
4import java.util.concurrent.ScheduledExecutorService;
5import java.util.concurrent.ScheduledFuture;
6import java.util.concurrent.TimeUnit;
7import java.util.concurrent.atomic.AtomicLong;
8
9public class CustomIntervalManager {
10
11 // Global map to store active scheduled futures for each custom interval
12 private static final Map<Long, ScheduledFuture<?>> intervalMap = new ConcurrentHashMap<>();
13
14 // Executor service for scheduling tasks
15 private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
16
17 // Counter for generating unique IDs
18 private static final AtomicLong idCounter = new AtomicLong(System.currentTimeMillis());
19
20 /**
21 * Creates a custom interval that executes a function repeatedly
22 * @param fn - The function to execute at each interval
23 * @param delay - Initial delay before the first execution (in milliseconds)
24 * @param period - Time between subsequent executions (in milliseconds)
25 * @return A unique identifier for this interval
26 */
27 public static long customInterval(Runnable fn, long delay, long period) {
28 // Generate unique ID for this interval
29 long intervalId = idCounter.incrementAndGet();
30
31 // Create a wrapper class to handle the recursive scheduling
32 IntervalExecutor executor = new IntervalExecutor(fn, delay, period, intervalId);
33
34 // Start the first execution
35 executor.scheduleNextExecution();
36
37 return intervalId;
38 }
39
40 /**
41 * Clears a custom interval created by customInterval
42 * @param id - The unique identifier of the interval to clear
43 */
44 public static void customClearInterval(long id) {
45 // Check if the interval exists in the map
46 if (intervalMap.containsKey(id)) {
47 // Cancel the scheduled task associated with this interval
48 ScheduledFuture<?> future = intervalMap.get(id);
49 if (future != null) {
50 future.cancel(false);
51 }
52
53 // Remove the interval from the map
54 intervalMap.remove(id);
55 }
56 }
57
58 /**
59 * Inner class to handle the recursive scheduling of interval executions
60 */
61 private static class IntervalExecutor {
62 private final Runnable fn;
63 private final long delay;
64 private final long period;
65 private final long intervalId;
66 private int executionCount;
67
68 /**
69 * Constructor for IntervalExecutor
70 * @param fn - The function to execute
71 * @param delay - Initial delay in milliseconds
72 * @param period - Period between executions in milliseconds
73 * @param intervalId - Unique identifier for this interval
74 */
75 public IntervalExecutor(Runnable fn, long delay, long period, long intervalId) {
76 this.fn = fn;
77 this.delay = delay;
78 this.period = period;
79 this.intervalId = intervalId;
80 this.executionCount = 0;
81 }
82
83 /**
84 * Recursive method that schedules the next timeout
85 * Each execution schedules the next one with calculated delay
86 */
87 public void scheduleNextExecution() {
88 // Calculate the total delay for the current execution
89 long currentDelay = delay + period * executionCount;
90
91 // Create scheduled task and store its reference in the map
92 ScheduledFuture<?> future = scheduler.schedule(() -> {
93 // Execute the provided function
94 fn.run();
95
96 // Increment the execution counter
97 executionCount++;
98
99 // Schedule the next execution
100 scheduleNextExecution();
101 }, currentDelay, TimeUnit.MILLISECONDS);
102
103 // Store the scheduled future in the map for later cancellation
104 intervalMap.put(intervalId, future);
105 }
106 }
107}
108
1#include <unordered_map>
2#include <functional>
3#include <chrono>
4#include <thread>
5#include <atomic>
6#include <memory>
7
8// Global map to store active timeout threads for each custom interval
9std::unordered_map<int, std::shared_ptr<std::atomic<bool>>> intervalMap;
10
11// Global counter for generating unique interval IDs
12std::atomic<int> nextIntervalId(1);
13
14/**
15 * Creates a custom interval that executes a function repeatedly
16 * @param fn - The function to execute at each interval
17 * @param delay - Initial delay before the first execution (in milliseconds)
18 * @param period - Time between subsequent executions (in milliseconds)
19 * @return A unique identifier for this interval
20 */
21int customInterval(std::function<void()> fn, int delay, int period) {
22 // Generate unique ID for this interval
23 int intervalId = nextIntervalId.fetch_add(1);
24
25 // Create a shared flag to control the execution
26 auto stopFlag = std::make_shared<std::atomic<bool>>(false);
27
28 // Store the stop flag in the map
29 intervalMap[intervalId] = stopFlag;
30
31 // Create a new thread to handle the interval execution
32 std::thread([fn, delay, period, stopFlag]() {
33 // Counter to track the number of executions
34 int executionCount = 0;
35
36 // Continue executing until stop flag is set
37 while (!stopFlag->load()) {
38 // Calculate the total delay for the current execution
39 int currentDelay = delay + period * executionCount;
40
41 // Sleep for the calculated delay
42 std::this_thread::sleep_for(std::chrono::milliseconds(currentDelay));
43
44 // Check if we should stop before executing
45 if (stopFlag->load()) {
46 break;
47 }
48
49 // Execute the provided function
50 fn();
51
52 // Increment the execution counter
53 executionCount++;
54 }
55 }).detach();
56
57 return intervalId;
58}
59
60/**
61 * Clears a custom interval created by customInterval
62 * @param id - The unique identifier of the interval to clear
63 */
64void customClearInterval(int id) {
65 // Check if the interval exists in the map
66 auto it = intervalMap.find(id);
67 if (it != intervalMap.end()) {
68 // Set the stop flag to terminate the thread
69 it->second->store(true);
70
71 // Remove the interval from the map
72 intervalMap.erase(it);
73 }
74}
75
1// Global map to store active timeout IDs for each custom interval
2const intervalMap = new Map<number, NodeJS.Timeout>();
3
4/**
5 * Creates a custom interval that executes a function repeatedly
6 * @param fn - The function to execute at each interval
7 * @param delay - Initial delay before the first execution (in milliseconds)
8 * @param period - Time between subsequent executions (in milliseconds)
9 * @returns A unique identifier for this interval
10 */
11function customInterval(fn: Function, delay: number, period: number): number {
12 // Generate unique ID for this interval
13 const intervalId = Date.now();
14
15 // Counter to track the number of executions
16 let executionCount = 0;
17
18 /**
19 * Recursive function that schedules the next timeout
20 * Each execution schedules the next one with calculated delay
21 */
22 function scheduleNextExecution(): void {
23 // Calculate the total delay for the current execution
24 const currentDelay = delay + period * executionCount;
25
26 // Create timeout and store its reference in the map
27 const timeoutId = setTimeout(() => {
28 // Execute the provided function
29 fn();
30
31 // Increment the execution counter
32 executionCount++;
33
34 // Schedule the next execution
35 scheduleNextExecution();
36 }, currentDelay);
37
38 // Store the timeout ID in the map for later cancellation
39 intervalMap.set(intervalId, timeoutId);
40 }
41
42 // Start the first execution
43 scheduleNextExecution();
44
45 return intervalId;
46}
47
48/**
49 * Clears a custom interval created by customInterval
50 * @param id - The unique identifier of the interval to clear
51 */
52function customClearInterval(id: number): void {
53 // Check if the interval exists in the map
54 if (intervalMap.has(id)) {
55 // Clear the timeout associated with this interval
56 const timeoutId = intervalMap.get(id)!;
57 clearTimeout(timeoutId);
58
59 // Remove the interval from the map
60 intervalMap.delete(id);
61 }
62}
63
Time and Space Complexity
Time Complexity:
-
customInterval
:O(1)
for each call. The function performs constant-time operations including creating a closure, callingDate.now()
, and initiating the first recursive call. Each subsequent timeout execution also takesO(1)
time to set up the next timeout and execute the callback functionfn()
. -
customClearInterval
:O(1)
. The function performs a hash map lookup withhas()
, retrieval withget()
, clearing the timeout, and deletion withdelete()
, all of which are constant-time operations on average for aMap
.
Space Complexity:
-
customInterval
:O(1)
per interval created. Each interval stores:- One entry in the
intervalMap
(key-value pair) - One closure maintaining the
count
,id
, and reference tofn
- One active timeout in the event loop at any given time
- One entry in the
-
customClearInterval
:O(1)
. No additional space is allocated; it only removes existing entries.
Overall Space Complexity: O(n)
where n
is the number of active intervals. The intervalMap
grows linearly with the number of intervals created and not yet cleared.
Note: There's a bug in the code - the recursive timeout calculation delay + period * count
causes increasingly longer delays between executions rather than maintaining a constant period. The timeout should likely be just period
after the initial delay
to achieve typical interval behavior.
Common Pitfalls
1. Thread Safety Issues with Shared State
The current implementation uses a global interval_map
dictionary that can be accessed by multiple threads simultaneously (main thread and timer threads). Python's GIL provides some protection, but operations like checking existence and deletion aren't atomic, potentially causing race conditions.
Problem Example:
# Thread 1: Clearing an interval
if id in interval_map: # Check passes
# Thread 2: Same interval naturally completes and removes itself
timer = interval_map[id] # KeyError - entry was just deleted!
Solution: Use a thread lock to ensure atomic operations:
import threading
interval_lock = threading.Lock()
interval_map: Dict[int, threading.Timer] = {}
def custom_clear_interval(id: int) -> None:
with interval_lock:
if id in interval_map:
timer = interval_map[id]
timer.cancel()
del interval_map[id]
2. Memory Leak from Uncanceled Timers
If intervals aren't explicitly cleared, the recursive nature keeps creating new timer objects indefinitely. The interval_map
continues to hold references, preventing garbage collection even after timers complete.
Problem Example:
# Creating many short-lived intervals without clearing them
for i in range(1000):
custom_interval(lambda: print("test"), 0.1, 0.1)
# Memory keeps growing as old timer references remain in interval_map
Solution: Clean up the map entry after each execution:
def schedule_next_execution():
current_delay = delay + period * execution_count[0]
def execute_and_reschedule():
# Remove old timer reference before creating new one
if interval_id in interval_map:
del interval_map[interval_id]
fn()
execution_count[0] += 1
schedule_next_execution()
timer = threading.Timer(current_delay, execute_and_reschedule)
timer.daemon = True
timer.start()
interval_map[interval_id] = timer
3. ID Collision Risk
Using int(time.time() * 1000000)
for ID generation can produce collisions when multiple intervals are created within the same microsecond, especially on systems with lower time resolution.
Problem Example:
# Creating intervals in rapid succession id1 = custom_interval(func1, 1, 1) id2 = custom_interval(func2, 1, 1) # Might get same ID! # id2 overwrites id1 in the map, making id1 unclearable
Solution: Use a counter with thread-safe increment:
import itertools
id_counter = itertools.count(1) # Thread-safe counter
def custom_interval(fn: Callable, delay: float, period: float) -> int:
interval_id = next(id_counter) # Guaranteed unique
# ... rest of implementation
4. Delay Drift Accumulation
The recursive approach calculates delay from the start time but doesn't account for execution time of fn()
. If fn()
takes significant time, the actual interval between executions differs from the expected pattern.
Problem Example:
def slow_function():
time.sleep(0.5) # Takes 500ms to execute
print("done")
# Expected: Execute at 1s, 2s, 3s...
# Actual: Execute at 1s, 2.5s, 4s... (includes execution time)
custom_interval(slow_function, 1, 1)
Solution: Track absolute start time and calculate delays from it:
def custom_interval(fn: Callable, delay: float, period: float) -> int:
interval_id = next(id_counter)
start_time = time.time()
execution_count = [0]
def schedule_next_execution():
# Calculate delay from absolute start time
target_time = start_time + delay + period * execution_count[0]
current_time = time.time()
actual_delay = max(0, target_time - current_time)
def execute_and_reschedule():
fn()
execution_count[0] += 1
schedule_next_execution()
timer = threading.Timer(actual_delay, execute_and_reschedule)
timer.daemon = True
timer.start()
interval_map[interval_id] = timer
schedule_next_execution()
return interval_id
Which of the following shows the order of node visit in a Breadth-first Search?
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!