2627. Debounce

MediumJavaScript
Leetcode Link

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 at 80ms (30 + 50)
  • Call at 60ms: Previous call cancelled, rescheduled to execute at 110ms (60 + 50)
  • Call at 100ms: Previous call cancelled, rescheduled to execute at 150ms (100 + 50)
  • Final execution happens at 150ms with the arguments from the 100ms call

Scenario 2: If t = 35ms and the function is called at times 30ms, 60ms, and 100ms:

  • Call at 30ms: Scheduled to execute at 65ms (30 + 35)
  • Call at 60ms: Previous call cancelled, rescheduled to execute at 95ms (60 + 35)
  • Call at 100ms: Previous call doesn't affect the 60ms call since it already executed at 95ms. This new call executes at 135ms (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Delay execution: We can't run the function immediately; we need to wait t milliseconds
  2. 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 for t milliseconds
    • Save the new timer ID for potential future cancellation

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 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

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 function
  • this preserves the context from when the debounced function was called
  • args 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:

  1. First call: timeout is undefined, skip cancellation, schedule execution
  2. Subsequent call within t ms: Cancel previous timer, schedule new execution
  3. If no calls for t ms: Timer completes, function executes
  4. 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 Evaluator

Example 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:

  1. 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
  2. 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"
  3. 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"
  4. 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 !== undefined is O(1)
  • Calling clearTimeout() is O(1)
  • Setting up a new timeout with setTimeout() is O(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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More