2627. Debounce
Problem Description
You need to implement a debounce function that takes two parameters: a function fn
and a time delay t
in milliseconds. The debounce function should return a new version of the original function with special timing behavior.
What is debouncing?
Debouncing delays the execution of a function by t
milliseconds after it's called. If the function is called again during this waiting period, the previous call is cancelled and a new waiting period begins.
How it works:
- When the debounced function is called, it starts a timer for
t
milliseconds - If the function is called again before the timer expires, the previous timer is cancelled and a new timer starts
- The function only executes when the timer completes without interruption
- The function receives all the parameters that were passed in the most recent call
Example scenarios:
Scenario 1: If t = 50ms
and the function is called at times 30ms
, 60ms
, and 100ms
:
- Call at
30ms
: Scheduled to execute at80ms
(30 + 50) - Call at
60ms
: Previous call cancelled, rescheduled to execute at110ms
(60 + 50) - Call at
100ms
: Previous call cancelled, rescheduled to execute at150ms
(100 + 50) - Final execution happens at
150ms
with the arguments from the100ms
call
Scenario 2: If t = 35ms
and the function is called at times 30ms
, 60ms
, and 100ms
:
- Call at
30ms
: Scheduled to execute at65ms
(30 + 35) - Call at
60ms
: Previous call cancelled, rescheduled to execute at95ms
(60 + 35) - Call at
100ms
: Previous call doesn't affect the60ms
call since it already executed at95ms
. This new call executes at135ms
(100 + 35)
The solution uses setTimeout
to delay execution and clearTimeout
to cancel pending executions when a new call comes in. The function maintains the correct this
context and passes along all arguments using apply
.
Intuition
The key insight is that we need to control when a function executes, not just what it does. Think of debouncing like waiting for someone to finish speaking before responding - you don't want to interrupt them mid-sentence, so you wait for a pause.
To build this behavior, we need two core capabilities:
- Delay execution: We can't run the function immediately; we need to wait
t
milliseconds - Cancel pending executions: If a new call comes in, we need to forget about the previous one
JavaScript's setTimeout
gives us the delay mechanism - it schedules a function to run after a specified time. The crucial realization is that setTimeout
returns a timer ID that we can use with clearTimeout
to cancel the scheduled execution.
This leads us to the pattern:
- Store the timer ID in a variable that persists between function calls (using closure)
- When the debounced function is called:
- If there's an existing timer, cancel it with
clearTimeout
- Create a new timer with
setTimeout
fort
milliseconds - Save the new timer ID for potential future cancellation
- If there's an existing timer, cancel it with
The closure is essential here - the returned function needs to "remember" the timer ID between calls. By declaring timeout
in the outer function scope, every call to the returned function can access and modify the same timer reference.
For preserving the original function's context and arguments, we use apply(this, args)
. This ensures that:
- The original function receives the correct
this
context - All arguments from the most recent call are passed through
- The function behaves exactly as expected, just with delayed timing
The beauty of this approach is its simplicity - we're essentially creating a "gatekeeper" that only lets the function execute after ensuring no one else is waiting in line.
Solution Approach
Let's walk through the implementation step by step:
1. Function Signature
function debounce(fn: F, t: number): F
The debounce function accepts the original function fn
and delay time t
, and returns a new function of the same type.
2. Timer Storage Using Closure
let timeout: ReturnType<typeof setTimeout> | undefined;
We declare a variable to store the timer ID outside the returned function but inside the debounce function. This creates a closure - the returned function will have access to this variable across multiple calls. Initially, it's undefined
to indicate no pending execution.
3. Return the Debounced Function
return function (...args) {
// implementation
};
We return an anonymous function that accepts any number of arguments using the rest parameter ...args
. This ensures we can handle functions with any parameter signature.
4. Cancel Previous Timer (if exists)
if (timeout !== undefined) {
clearTimeout(timeout);
}
Before scheduling a new execution, we check if there's an existing timer. If there is, we cancel it using clearTimeout
. This is the core debouncing behavior - cancelling the previous scheduled execution when a new call comes in.
5. Schedule New Execution
timeout = setTimeout(() => {
fn.apply(this, args);
}, t);
We create a new timer that will execute after t
milliseconds and store its ID in the timeout
variable. Inside the timer callback:
fn.apply(this, args)
executes the original functionthis
preserves the context from when the debounced function was calledargs
passes all the arguments from the most recent call
Data Structure Pattern:
The solution uses the Closure Pattern to maintain state between function calls. The timeout
variable acts as a simple state container that persists across invocations.
Algorithm Flow:
- First call:
timeout
is undefined, skip cancellation, schedule execution - Subsequent call within
t
ms: Cancel previous timer, schedule new execution - If no calls for
t
ms: Timer completes, function executes - Cycle repeats for future calls
This implementation ensures that the function only executes when there's been a "quiet period" of at least t
milliseconds with no new calls.
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 to see how debouncing works in practice.
Setup:
// Original function that logs a greeting
function greet(name) {
console.log(`Hello, ${name}!`);
}
// Create debounced version with 50ms delay
const debouncedGreet = debounce(greet, 50);
Timeline of calls:
// Time 0ms: First call debouncedGreet("Alice"); // → Timer A scheduled for 50ms // Time 20ms: Second call (within 50ms window) debouncedGreet("Bob"); // → Timer A cancelled // → Timer B scheduled for 70ms (20 + 50) // Time 30ms: Third call (still within window) debouncedGreet("Charlie"); // → Timer B cancelled // → Timer C scheduled for 80ms (30 + 50) // Time 80ms: Timer C completes // → Executes: greet("Charlie") // → Output: "Hello, Charlie!"
What happened internally:
-
First call at 0ms:
timeout
is undefined (no previous timer)- Skip cancellation step
- Create Timer A to execute at 50ms
- Store Timer A's ID in
timeout
-
Second call at 20ms:
timeout
contains Timer A's ID- Cancel Timer A with
clearTimeout(timeout)
- Create Timer B to execute at 70ms
- Update
timeout
with Timer B's ID - Note: "Alice" is forgotten; we now have "Bob"
-
Third call at 30ms:
timeout
contains Timer B's ID- Cancel Timer B with
clearTimeout(timeout)
- Create Timer C to execute at 80ms
- Update
timeout
with Timer C's ID - Note: "Bob" is forgotten; we now have "Charlie"
-
At 80ms:
- No new calls came in to interrupt
- Timer C completes
fn.apply(this, args)
executes with args = ["Charlie"]- Output: "Hello, Charlie!"
Key observations:
- Only the last call's arguments ("Charlie") were used
- The first two calls never executed because they were cancelled
- The function only ran once, after the "quiet period" of 50ms
- The total delay from first call to execution was 80ms (much longer than the 50ms delay) because calls kept resetting the timer
This demonstrates the core debouncing behavior: rapid successive calls result in only one execution with the most recent arguments, after the specified delay from the last call.
Solution Implementation
1from typing import Any, Callable
2import threading
3
4def debounce(fn: Callable[..., Any], t: float) -> Callable[..., None]:
5 """
6 Creates a debounced version of the provided function that delays its execution
7 until after the specified timeout has elapsed since the last time it was invoked.
8
9 Args:
10 fn: The function to debounce
11 t: The delay time in seconds (Python's Timer uses seconds, not milliseconds)
12
13 Returns:
14 A debounced version of the input function
15 """
16 # Store the timer object to track and cancel pending executions
17 timer_obj = None
18
19 def debounced_function(*args: Any, **kwargs: Any) -> None:
20 """
21 The wrapped function that implements debouncing logic.
22
23 Args:
24 *args: Positional arguments to pass to the original function
25 **kwargs: Keyword arguments to pass to the original function
26 """
27 # Access the timer object from the outer scope
28 nonlocal timer_obj
29
30 # Cancel any existing timer to prevent the previous pending execution
31 if timer_obj is not None:
32 timer_obj.cancel()
33
34 # Create a new timer to execute the function after the specified delay
35 # Note: t is divided by 1000 to convert milliseconds to seconds
36 timer_obj = threading.Timer(t / 1000, fn, args=args, kwargs=kwargs)
37 timer_obj.start()
38
39 return debounced_function
40
41
42# Example usage:
43# log = debounce(print, 100)
44# log('Hello') # cancelled
45# log('Hello') # cancelled
46# log('Hello') # Printed at t=100ms
47
1import java.util.concurrent.Executors;
2import java.util.concurrent.ScheduledExecutorService;
3import java.util.concurrent.ScheduledFuture;
4import java.util.concurrent.TimeUnit;
5import java.util.function.Consumer;
6
7/**
8 * A generic functional interface that can accept any type of argument
9 * and perform an action without returning a value.
10 *
11 * @param <T> The type of the input argument
12 */
13@FunctionalInterface
14interface F<T> {
15 void apply(T args);
16}
17
18/**
19 * Utility class that provides debouncing functionality for functions.
20 * Debouncing ensures that a function is only executed after a specified
21 * delay has passed since the last invocation attempt.
22 */
23public class Debounce {
24
25 /**
26 * Creates a debounced version of the provided function that delays its execution
27 * until after the specified timeout has elapsed since the last time it was invoked.
28 *
29 * @param <T> The type of arguments the function accepts
30 * @param fn The function to debounce
31 * @param t The delay time in milliseconds
32 * @return A debounced version of the input function
33 */
34 public static <T> F<T> debounce(F<T> fn, long t) {
35 // Create a single-threaded executor for scheduling delayed tasks
36 final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
37
38 // Array wrapper to hold the future reference (allows modification in lambda)
39 final ScheduledFuture<?>[] futureHolder = new ScheduledFuture<?>[1];
40
41 // Return a new function that wraps the original function with debouncing logic
42 return new F<T>() {
43 @Override
44 public void apply(T args) {
45 // Clear any existing scheduled task to cancel the previous pending execution
46 if (futureHolder[0] != null && !futureHolder[0].isDone()) {
47 futureHolder[0].cancel(false);
48 }
49
50 // Schedule a new task to execute the function after the specified delay
51 futureHolder[0] = executor.schedule(() -> {
52 // Execute the original function with the provided arguments
53 fn.apply(args);
54 }, t, TimeUnit.MILLISECONDS);
55 }
56 };
57 }
58
59 /**
60 * Example usage demonstration
61 */
62 public static void main(String[] args) throws InterruptedException {
63 // Create a debounced version of a print function with 100ms delay
64 F<String> log = debounce(message -> System.out.println(message), 100);
65
66 // These calls will be cancelled as new calls come in before the delay expires
67 log.apply("Hello 1"); // cancelled
68 log.apply("Hello 2"); // cancelled
69 log.apply("Hello 3"); // Logged at t=100ms after this call
70
71 // Wait to see the output
72 Thread.sleep(200);
73 System.exit(0);
74 }
75}
76
1#include <functional>
2#include <chrono>
3#include <thread>
4#include <memory>
5#include <mutex>
6#include <atomic>
7
8/**
9 * Creates a debounced version of the provided function that delays its execution
10 * until after the specified timeout has elapsed since the last time it was invoked.
11 *
12 * @tparam Func - The function type to debounce
13 * @param fn - The function to debounce
14 * @param t - The delay time in milliseconds
15 * @returns A debounced version of the input function
16 */
17template<typename Func>
18std::function<void()> debounce(Func fn, int t) {
19 // Shared state between all invocations of the returned function
20 auto timer_state = std::make_shared<struct TimerState>();
21
22 struct TimerState {
23 std::mutex mutex;
24 std::shared_ptr<std::thread> pending_thread;
25 std::atomic<bool> should_cancel{false};
26 };
27
28 // Return a new function that wraps the original function with debouncing logic
29 return [fn, t, timer_state]() {
30 std::lock_guard<std::mutex> lock(timer_state->mutex);
31
32 // Signal any existing thread to cancel its execution
33 timer_state->should_cancel = true;
34
35 // Wait for and clean up any existing thread
36 if (timer_state->pending_thread && timer_state->pending_thread->joinable()) {
37 timer_state->pending_thread->join();
38 }
39
40 // Reset the cancellation flag for the new thread
41 timer_state->should_cancel = false;
42
43 // Create a new thread to execute the function after the specified delay
44 timer_state->pending_thread = std::make_shared<std::thread>(
45 [fn, t, timer_state]() {
46 // Sleep for the specified duration
47 std::this_thread::sleep_for(std::chrono::milliseconds(t));
48
49 // Check if this execution should be cancelled
50 if (!timer_state->should_cancel) {
51 // Execute the original function
52 fn();
53 }
54 }
55 );
56
57 // Detach the thread to allow it to run independently
58 timer_state->pending_thread->detach();
59 };
60}
61
62/**
63 * Alternative implementation using variadic templates for functions with parameters
64 *
65 * @tparam R - Return type of the function
66 * @tparam Args - Parameter types of the function
67 * @param fn - The function to debounce
68 * @param t - The delay time in milliseconds
69 * @returns A debounced version of the input function
70 */
71template<typename R, typename... Args>
72std::function<void(Args...)> debounce(std::function<R(Args...)> fn, int t) {
73 // Shared state between all invocations of the returned function
74 auto timer_state = std::make_shared<struct TimerState>();
75
76 struct TimerState {
77 std::mutex mutex;
78 std::shared_ptr<std::thread> pending_thread;
79 std::atomic<bool> should_cancel{false};
80 };
81
82 // Return a new function that wraps the original function with debouncing logic
83 return [fn, t, timer_state](Args... args) {
84 std::lock_guard<std::mutex> lock(timer_state->mutex);
85
86 // Signal any existing thread to cancel its execution
87 timer_state->should_cancel = true;
88
89 // Wait for and clean up any existing thread
90 if (timer_state->pending_thread && timer_state->pending_thread->joinable()) {
91 timer_state->pending_thread->join();
92 }
93
94 // Reset the cancellation flag for the new thread
95 timer_state->should_cancel = false;
96
97 // Create a new thread to execute the function after the specified delay
98 timer_state->pending_thread = std::make_shared<std::thread>(
99 [fn, t, timer_state, args...]() {
100 // Sleep for the specified duration
101 std::this_thread::sleep_for(std::chrono::milliseconds(t));
102
103 // Check if this execution should be cancelled
104 if (!timer_state->should_cancel) {
105 // Execute the original function with the captured arguments
106 fn(args...);
107 }
108 }
109 );
110
111 // Detach the thread to allow it to run independently
112 timer_state->pending_thread->detach();
113 };
114}
115
116/**
117 * Example usage:
118 * auto log = debounce([]() { std::cout << "Hello" << std::endl; }, 100);
119 * log(); // cancelled
120 * log(); // cancelled
121 * log(); // Logged at t=100ms
122 */
123
1// Type definition for a function that accepts any parameters and returns any value
2type F = (...p: any[]) => any;
3
4/**
5 * Creates a debounced version of the provided function that delays its execution
6 * until after the specified timeout has elapsed since the last time it was invoked.
7 *
8 * @param fn - The function to debounce
9 * @param t - The delay time in milliseconds
10 * @returns A debounced version of the input function
11 */
12function debounce(fn: F, t: number): F {
13 // Store the timeout ID to track and cancel pending executions
14 let timeoutId: ReturnType<typeof setTimeout> | undefined;
15
16 // Return a new function that wraps the original function with debouncing logic
17 return function (this: any, ...args: any[]): void {
18 // Clear any existing timeout to cancel the previous pending execution
19 if (timeoutId !== undefined) {
20 clearTimeout(timeoutId);
21 }
22
23 // Set a new timeout to execute the function after the specified delay
24 timeoutId = setTimeout(() => {
25 // Execute the original function with the correct context and arguments
26 fn.apply(this, args);
27 }, t);
28 };
29}
30
31/**
32 * Example usage:
33 * const log = debounce(console.log, 100);
34 * log('Hello'); // cancelled
35 * log('Hello'); // cancelled
36 * log('Hello'); // Logged at t=100ms
37 */
38
Time and Space Complexity
Time Complexity: O(1)
for each function call
The debounce function itself executes in constant time for each invocation:
- Checking if
timeout !== undefined
isO(1)
- Calling
clearTimeout()
isO(1)
- Setting up a new timeout with
setTimeout()
isO(1)
- The actual execution of the wrapped function
fn
happens asynchronously and its complexity depends on the function being debounced, but the debounce wrapper operations themselves are all constant time
Space Complexity: O(1)
The space usage remains constant regardless of input:
- The closure maintains a single
timeout
variable that persists between calls - The
args
array reference is stored but not copied (just a reference to the arguments passed) - Each call either clears the previous timeout or creates a new one, but only one timeout exists at any given time
- The closure itself occupies constant space in memory
Note that if multiple debounced functions are created, each maintains its own closure with its own timeout
variable, but for a single debounced function instance, the space complexity remains O(1)
.
Common Pitfalls
1. Loss of Return Values
The debounced function returns None
instead of the original function's return value. This is a fundamental limitation because the function executes asynchronously after a delay, making it impossible to return the result synchronously.
Problem Example:
def calculate(x, y):
return x + y
debounced_calc = debounce(calculate, 100)
result = debounced_calc(5, 3) # result is None, not 8
Solution: Use callbacks or promises/futures to handle results:
from concurrent.futures import Future
import threading
def debounce_with_future(fn, t):
timer_obj = None
future_obj = None
def debounced_function(*args, **kwargs):
nonlocal timer_obj, future_obj
if timer_obj is not None:
timer_obj.cancel()
if future_obj:
future_obj.cancel()
future_obj = Future()
def execute():
try:
result = fn(*args, **kwargs)
future_obj.set_result(result)
except Exception as e:
future_obj.set_exception(e)
timer_obj = threading.Timer(t / 1000, execute)
timer_obj.start()
return future_obj
return debounced_function
2. Context (self
) Binding Issues in Class Methods
When debouncing instance methods, the self
context can be lost or incorrectly bound.
Problem Example:
class Counter:
def __init__(self):
self.count = 0
self.increment = debounce(self.increment, 100) # Wrong!
def increment(self):
self.count += 1
print(f"Count: {self.count}")
counter = Counter() # RecursionError or AttributeError
Solution: Create the debounced method properly:
class Counter:
def __init__(self):
self.count = 0
self._increment = self.increment_impl
self.increment = debounce(self.increment_impl, 100)
def increment_impl(self):
self.count += 1
print(f"Count: {self.count}")
3. Memory Leaks with Long-Running Timers
If a debounced function is called but the timer never completes (e.g., the function keeps getting called), the timer threads accumulate without being properly cleaned up.
Problem Example:
# In a loop that runs frequently
for i in range(10000):
debounced_func(i) # Creates 10000 timer threads
time.sleep(0.001) # Each call cancels the previous
Solution: Add cleanup mechanism:
def debounce_with_cleanup(fn, t):
timer_obj = None
def debounced_function(*args, **kwargs):
nonlocal timer_obj
if timer_obj is not None:
timer_obj.cancel()
timer_obj.join(timeout=0.001) # Wait for thread cleanup
timer_obj = threading.Timer(t / 1000, fn, args=args, kwargs=kwargs)
timer_obj.daemon = True # Ensure thread doesn't prevent program exit
timer_obj.start()
# Add cleanup method
debounced_function.cancel = lambda: timer_obj.cancel() if timer_obj else None
return debounced_function
4. Thread Safety Issues
Multiple threads calling the debounced function simultaneously can cause race conditions when accessing and modifying the timer_obj
.
Problem Example:
debounced_print = debounce(print, 100)
# Multiple threads calling simultaneously
thread1 = threading.Thread(target=lambda: debounced_print("Thread 1"))
thread2 = threading.Thread(target=lambda: debounced_print("Thread 2"))
thread1.start()
thread2.start()
# Race condition: timer_obj might be accessed/modified simultaneously
Solution: Add thread synchronization:
def debounce_thread_safe(fn, t):
timer_obj = None
lock = threading.Lock()
def debounced_function(*args, **kwargs):
nonlocal timer_obj
with lock:
if timer_obj is not None:
timer_obj.cancel()
timer_obj = threading.Timer(t / 1000, fn, args=args, kwargs=kwargs)
timer_obj.start()
return debounced_function
Depth first search is equivalent to which of the tree traversal order?
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!