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
tmilliseconds - 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
150mswith the arguments from the100mscall
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 the60mscall 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
tmilliseconds - 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
setTimeoutfortmilliseconds - 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
thiscontext - 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 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 471import 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} 761#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 */ 1231// 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 */ 38Solution 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 functionthispreserves the context from when the debounced function was calledargspasses 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:
timeoutis undefined, skip cancellation, schedule execution - Subsequent call within
tms: Cancel previous timer, schedule new execution - If no calls for
tms: 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 5-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:
timeoutis 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:
timeoutcontains Timer A's ID- Cancel Timer A with
clearTimeout(timeout) - Create Timer B to execute at 70ms
- Update
timeoutwith Timer B's ID - Note: "Alice" is forgotten; we now have "Bob"
-
Third call at 30ms:
timeoutcontains Timer B's ID- Cancel Timer B with
clearTimeout(timeout) - Create Timer C to execute at 80ms
- Update
timeoutwith 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.
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 !== undefinedisO(1) - Calling
clearTimeout()isO(1) - Setting up a new timeout with
setTimeout()isO(1) - The actual execution of the wrapped function
fnhappens 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
timeoutvariable that persists between calls - The
argsarray 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
Which data structure is used to implement priority queue?
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
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!