Facebook Pixel

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

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 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More