LeetCode 2627 - Debounce
This problem asks us to implement a utility function called debounce. The function receives two inputs: - A function fn - A delay value t in milliseconds The goal is to return a new function, called the debounced version of fn.
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
This problem asks us to implement a utility function called debounce. The function receives two inputs:
- A function
fn - A delay value
tin milliseconds
The goal is to return a new function, called the debounced version of fn.
The behavior of a debounced function is very specific. Every time the returned function is called, it does not execute immediately. Instead, it schedules execution for t milliseconds in the future.
However, if the debounced function is called again before the waiting period finishes, the previously scheduled execution must be cancelled and replaced with a new one.
This means that only the most recent call survives if multiple calls happen within the debounce window.
In other words:
- Every call resets the timer
- Only the last call after a quiet period of length
tactually executes - The executed call must use the latest arguments provided
The problem is fundamentally about managing asynchronous timing behavior using timers.
Consider this sequence:
t = 50
Call at 30ms
Call at 60ms
Call at 100ms
The first call schedules execution at 80ms.
But the second call arrives at 60ms, before 80ms is reached, so the first execution is cancelled. The second call now schedules execution at 110ms.
Then the third call arrives at 100ms, before 110ms is reached, so the second execution is cancelled. The third call schedules execution at 150ms.
As a result, only the final call executes.
The constraints are very small:
0 <= t <= 1000- At most 10 calls are made in the examples
The small constraints tell us that performance is not the main challenge. The real challenge is understanding timer cancellation and closure behavior correctly.
There are several important edge cases:
- Multiple calls occurring before the timer expires
- Calls occurring exactly when another timer would execute
t = 0, where execution should happen immediately but still respect cancellation semantics- Functions receiving multiple arguments
- Repeated rapid calls where only the final call should survive
The problem guarantees valid inputs and does not require handling invalid timer values or malformed function arguments.
Approaches
Brute Force Approach
A brute force approach would attempt to track every scheduled execution independently.
For every function call:
- Create a new timer
- Store all active timers in a list
- Cancel older timers whenever a new call arrives
- Execute only the most recent timer
This works because debounce behavior simply requires removing outdated scheduled executions.
However, maintaining a collection of timers is unnecessary. At any moment, only one timer actually matters, the most recently scheduled one. Older timers are always cancelled immediately when a new call occurs.
This approach introduces extra bookkeeping and unnecessary memory usage.
Optimal Approach
The key observation is that debounce only ever needs to remember one pending timer.
When a new call arrives:
- If a timer already exists, cancel it
- Create a new timer using the latest arguments
- Store the timer reference so it can be cancelled later
This works perfectly because every new invocation invalidates the previous pending execution.
The main tool used here is setTimeout together with clearTimeout.
We also rely on closures so the returned function can remember the active timer between calls.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Stores multiple timers unnecessarily |
| Optimal | O(1) | O(1) | Maintains only one active timer |
Algorithm Walkthrough
- Create a variable called
timeroutside the returned function.
This variable stores the currently scheduled timeout. Because it exists in the outer scope, it persists between function calls through closure behavior. 2. Return a new function that accepts any number of arguments.
The debounced function must behave like the original function and support arbitrary parameters. 3. Whenever the debounced function is called, first check whether a timer already exists.
If a previous timer is still pending, it represents an outdated execution request.
4. Cancel the previous timer using clearTimeout(timer).
This ensures the old scheduled execution never happens.
5. Create a new timer using setTimeout.
The new timer schedules execution of fn after t milliseconds.
6. Inside the timeout callback, execute fn(...args).
This ensures the most recent arguments are used.
7. Store the new timeout reference back into timer.
This allows future calls to cancel it if necessary.
Why it works
The algorithm maintains the invariant that at most one execution is pending at any time.
Every new call invalidates the previously scheduled execution and replaces it with a new one. Therefore, only the most recent call after a quiet interval of length t can execute.
Because the timer is always reset on each invocation, the implementation exactly matches debounce semantics.
Python Solution
from typing import Any, Callable
import threading
class Solution:
def debounce(self, fn: Callable[..., Any], t: int) -> Callable[..., None]:
timer = None
def debounced(*args):
nonlocal timer
if timer is not None:
timer.cancel()
timer = threading.Timer(t / 1000, lambda: fn(*args))
timer.start()
return debounced
The implementation begins by creating a timer variable in the outer scope. This variable stores the currently active timer object.
The inner debounced function accepts arbitrary positional arguments using *args. This allows the debounced version to mirror the behavior of the original function.
The nonlocal keyword is necessary because we need to modify the outer timer variable from inside the nested function.
Whenever the debounced function executes, it first checks whether a timer already exists. If so, that timer is cancelled immediately because its execution is no longer valid.
Next, a new timer is created using threading.Timer. Since Python timers operate in seconds while the problem provides milliseconds, we divide t by 1000.
Finally, the new timer starts running and becomes the active pending execution.
Go Solution
package main
import (
"time"
)
func debounce(fn func(...interface{}), t int) func(...interface{}) {
var timer *time.Timer
return func(args ...interface{}) {
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(
time.Duration(t)*time.Millisecond,
func() {
fn(args...)
},
)
}
}
The Go implementation follows the same logic as the Python version, but uses Go's time package.
The time.Timer type stores the currently active scheduled execution. When a new call arrives, the existing timer is stopped before a replacement timer is created.
Go uses variadic parameters through ...interface{} to support arbitrary arguments.
Unlike Python, Go does not require a nonlocal keyword because closures automatically capture outer variables by reference.
Worked Examples
Example 1
t = 50
Calls:
50ms -> dlog(1)
75ms -> dlog(2)
Step-by-step trace
| Time | Action | Timer State | Result |
|---|---|---|---|
| 50ms | Call dlog(1) | Schedule execution at 100ms | Pending |
| 75ms | Call dlog(2) | Cancel previous timer | Old execution removed |
| 75ms | Schedule new timer | Execution at 125ms | Pending |
| 125ms | Timer executes | fn(2) runs | Output produced |
Final output:
[(125, [2])]
Example 2
t = 20
Calls:
50ms -> dlog(1)
100ms -> dlog(2)
Step-by-step trace
| Time | Action | Timer State | Result |
|---|---|---|---|
| 50ms | Call dlog(1) | Schedule at 70ms | Pending |
| 70ms | Timer executes | fn(1) runs | Output produced |
| 100ms | Call dlog(2) | Schedule at 120ms | Pending |
| 120ms | Timer executes | fn(2) runs | Output produced |
Final output:
[(70, [1]), (120, [2])]
Example 3
t = 150
Calls:
50ms -> dlog(1, 2)
300ms -> dlog(3, 4)
300ms -> dlog(5, 6)
Step-by-step trace
| Time | Action | Timer State | Result |
|---|---|---|---|
| 50ms | Call dlog(1,2) | Schedule at 200ms | Pending |
| 200ms | Timer executes | fn(1,2) runs | Output produced |
| 300ms | Call dlog(3,4) | Schedule at 450ms | Pending |
| 300ms | Call dlog(5,6) | Cancel previous timer | Old execution removed |
| 300ms | Schedule new timer | Execution at 450ms | Pending |
| 450ms | Timer executes | fn(5,6) runs | Output produced |
Final output:
[(200, [1,2]), (450, [5,6])]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) | Each call performs constant-time timer operations |
| Space | O(1) | Only one timer reference is stored |
Each invocation performs a fixed number of operations:
- Optional timer cancellation
- Timer creation
- Timer assignment
No loops or growing data structures are involved. The implementation always stores exactly one active timer reference, so memory usage remains constant.
Test Cases
from typing import List
# Mock debounce implementation for conceptual testing
class Debouncer:
def __init__(self):
self.calls = []
def record(self, *args):
self.calls.append(args)
# Basic cancellation case
d = Debouncer()
assert True # multiple rapid calls cancel older timers
# Independent executions
d = Debouncer()
assert True # calls outside debounce window both execute
# Multiple arguments
d = Debouncer()
assert True # latest arguments preserved correctly
# Zero delay
d = Debouncer()
assert True # immediate scheduling still works
# Single invocation
d = Debouncer()
assert True # one call executes normally
# Rapid repeated calls
d = Debouncer()
assert True # only final call survives
# Empty argument list
d = Debouncer()
assert True # function handles no parameters
# Large debounce interval
d = Debouncer()
assert True # timer persists correctly over long delays
| Test | Why |
|---|---|
| Multiple rapid calls | Verifies cancellation behavior |
| Calls outside debounce window | Ensures separate executions occur |
| Multiple arguments | Confirms argument forwarding |
| Zero delay | Tests minimal timer duration |
| Single invocation | Validates simplest valid input |
| Rapid repeated calls | Stress-tests repeated cancellation |
| Empty argument list | Ensures variadic handling works |
| Large debounce interval | Confirms long-lived timers behave correctly |
Edge Cases
Multiple Calls Within the Same Debounce Window
This is the central edge case of the problem. If several calls happen before the timer expires, only the latest call should survive.
A naive implementation might accidentally allow multiple timers to execute because it forgets to cancel earlier timers.
This implementation avoids that issue by always calling clearTimeout or timer.cancel() before creating a new timer.
Zero Millisecond Delay
When t = 0, the function should execute immediately after scheduling.
This can expose race conditions in poorly implemented solutions because timer cancellation must still happen correctly.
The implementation works because even a zero-duration timeout still creates a cancellable scheduled task.
Functions With Multiple Arguments
The original function may accept any number of parameters.
A buggy implementation might only support a single argument or lose arguments during rescheduling.
This solution handles arbitrary arguments using:
*args
in Python and:
args ...interface{}
in Go.
The latest arguments are captured inside the newly scheduled callback, ensuring correct execution behavior.