LeetCode 2805 - Custom Interval
This problem asks us to implement a custom version of setInterval, but with a key difference. Instead of using a fixed delay between executions, the delay grows linearly according to the formula: where count starts at 0 and increases after every execution.
Difficulty: 🟡 Medium
Topics: —
Solution
LeetCode 2805 - Custom Interval
Problem Understanding
This problem asks us to implement a custom version of setInterval, but with a key difference. Instead of using a fixed delay between executions, the delay grows linearly according to the formula:
$$delay + period \times count$$
where count starts at 0 and increases after every execution.
The function customInterval(fn, delay, period) should repeatedly execute the callback function fn. The first execution happens after delay milliseconds. After that execution completes, the next execution should be scheduled after delay + period * 1 milliseconds. The third execution should be scheduled after delay + period * 2 milliseconds, and so on.
The function returns an identifier id, which can later be passed to customClearInterval(id) to stop any future executions.
Unlike a normal interval where every execution occurs after a constant amount of time, this interval becomes progressively larger. The waiting times form an arithmetic progression:
- First wait:
delay - Second wait:
delay + period - Third wait:
delay + 2 * period - Fourth wait:
delay + 3 * period
and so forth.
The examples demonstrate that the execution times are cumulative. For example, if:
delay = 50period = 20
then:
- First execution occurs at
50 - Second execution occurs at
50 + 70 = 120 - Third execution occurs at
120 + 90 = 210
The constraints are very small, with delays ranging only from 20 to 250 milliseconds. However, this is not really an algorithmic problem about handling large inputs. Instead, it is a JavaScript timing and scheduling problem. The main challenge is correctly managing recursive scheduling and cancellation.
Several edge cases are important:
- The interval may be cancelled before the next scheduled execution occurs.
- The callback should stop immediately after cancellation.
- The returned identifier must contain enough information to cancel future timers.
- Since Node.js returns timer objects rather than numeric IDs, the implementation cannot assume the timer identifier is a number.
Approaches
Brute Force Approach
A naive idea would be to repeatedly create all future timeout schedules in advance.
For example, we could calculate:
- First timeout at
delay - Second timeout at
delay + (delay + period) - Third timeout at
delay + (delay + 2 * period)
and continue scheduling many future timers immediately.
This approach produces the correct execution times because each future execution time can be computed directly.
However, it has several problems:
- We do not know how long the interval should continue.
- We would need to store many timeout IDs.
- Cancelling becomes cumbersome because every scheduled timeout must be tracked and cleared.
- Potentially unbounded scheduling wastes memory.
Because the interval is conceptually infinite until cancelled, pre-scheduling everything is not practical.
Optimal Approach
The key observation is that only the next execution needs to be scheduled.
When a timeout fires:
- Execute
fn. - Increase the execution count.
- Compute the next delay using the formula.
- Schedule exactly one new timeout.
This creates a chain of timeouts. At any moment, there is only one active timer.
Cancellation becomes simple because we only need to clear the currently scheduled timeout and prevent future scheduling.
This approach naturally matches the problem's requirements and uses constant memory.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(k) | O(k) | Pre-schedules many future timeouts and stores all timer IDs |
| Optimal | O(1) per execution | O(1) | Maintains only the next scheduled timeout |
Here, k represents the number of executions that occur before cancellation.
Algorithm Walkthrough
- Create a counter variable initialized to
0. This counter tracks how many times the callback has already executed. - Create a state object that stores:
- Whether the interval has been cancelled.
- The currently active timeout ID.
- Define a helper function called
schedule. - Inside
schedule, create a timeout using the provided wait time. - When the timeout fires, first check whether the interval has been cancelled. If it has, stop immediately.
- Execute the callback function
fn. - Increment the execution counter because one execution has completed.
- Compute the next delay using:
$$delay + period \times count$$
where count now represents the number of completed executions.
9. Schedule another timeout using this newly computed delay.
10. Start the process by scheduling the first timeout with delay equal to delay.
11. Return the state object as the interval identifier.
12. In customClearInterval, mark the interval as cancelled and clear the currently active timeout.
Why it works
The algorithm maintains the invariant that exactly one timeout represents the next scheduled execution. After each execution, the counter is incremented and the next delay is computed according to the required formula. Therefore every execution occurs after the correct waiting time, and cancellation immediately prevents any future scheduling. Since each delay is calculated from the current execution count, the sequence of waits exactly matches the arithmetic progression defined in the problem.
Python Solution
Although this problem is designed for JavaScript, the equivalent Python implementation is shown below to illustrate the logic.
from threading import Timer
from typing import Callable, Dict, Any
def customInterval(fn: Callable[[], None], delay: int, period: int) -> Dict[str, Any]:
count = 0
state = {
"cancelled": False,
"timer": None,
}
def schedule(wait_ms: int) -> None:
nonlocal count
def callback() -> None:
nonlocal count
if state["cancelled"]:
return
fn()
count += 1
next_delay = delay + period * count
schedule(next_delay)
timer = Timer(wait_ms / 1000.0, callback)
state["timer"] = timer
timer.start()
schedule(delay)
return state
def customClearInterval(interval_id: Dict[str, Any]) -> None:
interval_id["cancelled"] = True
timer = interval_id.get("timer")
if timer is not None:
timer.cancel()
The implementation stores all interval state inside a dictionary. The count variable tracks how many executions have occurred. Every timeout callback executes the function, increments the count, computes the next delay, and recursively schedules another timer.
Only one timer exists at any time. The cancellation function sets a flag and cancels the currently active timer, preventing future executions.
Go Solution
Although LeetCode expects JavaScript for this problem, the same logic can be expressed in Go as follows.
package main
import (
"sync"
"time"
)
type Interval struct {
mu sync.Mutex
count int
cancelled bool
timer *time.Timer
delay int
period int
fn func()
}
func customInterval(fn func(), delay int, period int) *Interval {
interval := &Interval{
fn: fn,
delay: delay,
period: period,
}
var schedule func(int)
schedule = func(wait int) {
interval.timer = time.AfterFunc(
time.Duration(wait)*time.Millisecond,
func() {
interval.mu.Lock()
if interval.cancelled {
interval.mu.Unlock()
return
}
fn()
interval.count++
nextDelay := interval.delay + interval.period*interval.count
interval.mu.Unlock()
schedule(nextDelay)
},
)
}
schedule(delay)
return interval
}
func customClearInterval(id *Interval) {
id.mu.Lock()
defer id.mu.Unlock()
id.cancelled = true
if id.timer != nil {
id.timer.Stop()
}
}
The Go version uses time.AfterFunc to create delayed callbacks. A mutex is required because timer callbacks execute on separate goroutines. The mutex protects shared state such as the cancellation flag, execution count, and timer reference.
Worked Examples
Example 1
Input:
delay = 50
period = 20
cancelTime = 225
| Execution | count Before | Delay Used | Execution Time |
|---|---|---|---|
| 1 | 0 | 50 | 50 |
| 2 | 1 | 70 | 120 |
| 3 | 2 | 90 | 210 |
After the third execution:
next delay = 50 + 20 × 3
= 110
The next execution would occur at:
210 + 110 = 320
However, cancellation occurs at 225, so execution four never happens.
Result:
[50, 120, 210]
Example 2
Input:
delay = 20
period = 20
cancelTime = 150
| Execution | count Before | Delay Used | Execution Time |
|---|---|---|---|
| 1 | 0 | 20 | 20 |
| 2 | 1 | 40 | 60 |
| 3 | 2 | 60 | 120 |
The next delay would be:
20 + 20 × 3 = 80
Next execution time:
120 + 80 = 200
Since cancellation occurs at 150, execution four never occurs.
Result:
[20, 60, 120]
Example 3
Input:
delay = 100
period = 200
cancelTime = 500
| Execution | count Before | Delay Used | Execution Time |
|---|---|---|---|
| 1 | 0 | 100 | 100 |
| 2 | 1 | 300 | 400 |
The next delay would be:
100 + 200 × 2 = 500
The third execution would occur at:
400 + 500 = 900
Cancellation happens at 500, so the third execution never occurs.
Result:
[100, 400]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) per execution | Each callback performs a constant amount of work |
| Space | O(1) | Only a counter, cancellation flag, and current timer are stored |
The algorithm never stores information proportional to the number of executions. At any moment there is only one active timeout and a few bookkeeping variables, so memory usage remains constant. Each callback execution performs a fixed number of operations regardless of how many times the interval has already fired.
Test Cases
# Example 1
assert [50, 120, 210] == [50, 120, 210] # basic arithmetic progression
# Example 2
assert [20, 60, 120] == [20, 60, 120] # equal delay and period
# Example 3
assert [100, 400] == [100, 400] # large period
# Boundary values
assert 20 + 20 * 0 == 20 # minimum delay and period
assert 250 + 250 * 0 == 250 # maximum delay and period
# First reschedule calculation
delay = 50
period = 20
count = 1
assert delay + period * count == 70 # second interval
# Multiple interval growth
delay = 20
period = 30
assert delay + period * 4 == 140 # fifth scheduled delay
# Cancellation before next execution
cancel_time = 100
next_execution = 120
assert next_execution > cancel_time # next callback should not fire
# Large execution count formula
delay = 250
period = 250
count = 10
assert delay + period * count == 2750 # arithmetic progression remains correct
| Test | Why |
|---|---|
| Example 1 | Verifies increasing interval timings |
| Example 2 | Verifies equal delay and period |
| Example 3 | Verifies large period values |
| Minimum bounds | Tests smallest valid inputs |
| Maximum bounds | Tests largest valid inputs |
| First reschedule | Validates count increment logic |
| Multiple interval growth | Verifies arithmetic progression |
| Cancellation check | Ensures future execution is prevented |
| Large count formula | Verifies delay computation remains correct |
Edge Cases
Cancellation Immediately Before a Scheduled Execution
A common bug occurs when cancellation happens just before the next timeout fires. If the implementation only clears the current timeout but does not maintain a cancellation flag, a race condition could allow another callback to schedule a future timer. The solution avoids this by storing a cancelled flag and checking it before executing the callback.
Node.js Timer Objects Instead of Numeric IDs
Many implementations assume setTimeout returns a number. In browsers this is usually true, but in Node.js it returns a timer object. The solution treats the returned value as an opaque identifier and stores it directly, making the implementation compatible with both environments.
Very Large Execution Counts
As the interval continues running, the delay grows according to the arithmetic progression. A buggy implementation might accidentally reuse the same delay every time, effectively behaving like setInterval. The solution recomputes the delay after every execution using the updated execution count, guaranteeing the correct increasing sequence of delays.
Only One Active Timer
If multiple future timeouts are scheduled simultaneously, cancellation becomes difficult and memory usage grows unnecessarily. The implementation maintains exactly one active timeout at all times. This ensures constant space usage and makes cancellation straightforward.