アルゴリズム概要

問題の説明

関数 fn と遅延時間 t(ミリ秒)を受け取り、デバウンスされた関数を返す。 デバウンスされた関数は以下の性質を持つ:

  • 実行が t ミリ秒遅延される
  • 遅延時間内に再度呼び出されると、前の実行がキャンセルされる
  • 最後の呼び出しから t ミリ秒後に実行される

入出力例

Example 1: t = 50ms

calls = [
  {"t": 50, "inputs": [1]},
  {"t": 75, "inputs": [2]}
]
Output: [{"t": 125, "inputs": [2]}]

説明: 1回目の呼び出しは2回目によってキャンセルされる
     2回目は75ms + 50ms = 125msに実行される

Example 2: t = 20ms

calls = [
  {"t": 50, "inputs": [1]},
  {"t": 100, "inputs": [2]}
]
Output: [{"t": 70, "inputs": [1]}, {"t": 120, "inputs": [2]}]

説明: 1回目は50ms + 20ms = 70msに実行
     2回目は100ms + 20ms = 120msに実行

戦略

  • threading.Timer を使った遅延実行とキャンセル制御
  • クロージャで Timer オブジェクトを保持
  • 関数が呼ばれるたびに、既存のタイマーをキャンセル
  • 新しいタイマーを t/1000 秒後にセット
  • 最新の引数をクロージャで保持

主要ポイント

⏱️ 時間計算量

O(1) per call

💾 空間計算量

O(1) - タイマー1つのみ

ステップバイステップ解説

Python実装

from __future__ import annotations
from typing import Callable, Any
from threading import Timer


def debounce(fn: Callable[..., Any], t: float) -> Callable[..., None]:
    """
    関数の実行をデバウンスする(遅延実行&キャンセル機能付き)

    Args:
        fn: デバウンス対象の関数
        t: 遅延時間(ミリ秒)

    Returns:
        デバウンスされた関数

    Complexity:
        Time: O(1) per call
        Space: O(1)
    """
    # タイマーオブジェクトを保持するクロージャ変数
    timer: Timer | None = None

    def debounced_func(*args: Any, **kwargs: Any) -> None:
        nonlocal timer

        # 既存のタイマーがあればキャンセル
        if timer is not None:
            timer.cancel()

        # 新しいタイマーをセット(t/1000 秒後に fn を実行)
        # threading.Timer は秒単位なので、ミリ秒を秒に変換
        timer = Timer(t / 1000.0, fn, args=args, kwargs=kwargs)
        timer.start()

    return debounced_func


# LeetCode形式の実装例
class Solution:
    """
    LeetCode形式のラッパークラス
    実際のLeetCodeにはこの問題はJavaScript/TypeScriptのみだが、
    Pythonで同等の機能を提供
    """

    def debounce(self, fn: Callable[..., Any], t: int) -> Callable[..., None]:
        """
        Args:
            fn: デバウンス対象の関数
            t: 遅延時間(ミリ秒、整数)

        Returns:
            デバウンスされた関数
        """
        timer: Timer | None = None

        def debounced(*args: Any, **kwargs: Any) -> None:
            nonlocal timer

            # 基底条件: タイマーが存在すればキャンセル
            if timer is not None:
                timer.cancel()

            # 遷移: 新しいタイマーを作成して開始
            # t ミリ秒 = t/1000 秒
            timer = Timer(t / 1000.0, fn, args=args, kwargs=kwargs)
            timer.start()

        return debounced

フローチャート

デバウンス呼び出し タイマーあり? timer != None はい タイマーキャンセル timer.cancel() いいえ 新規タイマー Timer(t/1000) 引数保存 クロージャに保持 タイマー開始 timer.start() t ms 待機後 fn(*args, **kwargs) 実行

フローの説明:
1. デバウンス関数が呼ばれると、まず既存のタイマーの有無を確認
2. タイマーがあれば即座にキャンセル(前の実行を中止)
3. 新しいタイマーを t/1000 秒後に設定
4. 呼び出し時の引数をクロージャに保存
5. タイマーを開始し、t ミリ秒待機
6. 待機完了後、保存された引数で元の関数 fn を実行

計算量分析

時間計算量

O(1) per call

  • タイマーのキャンセル: O(1)
  • 新規タイマーの生成: O(1)
  • 実行時: O(f) where f は元の関数 fn の計算量

空間計算量

O(1)

  • タイマーオブジェクト1つと引数のタプル/辞書のみ
  • 引数のサイズ: O(args_size) だが、これは呼び出し元の責任

実装比較

実装方式 メリット デメリット 用途
threading.Timer
(本実装)
✅ 同期コードで使いやすい
✅ 追加の依存なし
❌ スレッドオーバーヘッド 汎用的な用途
asyncio ✅ スレッドなしで軽量
✅ 大量の同時debounce処理に有利
❌ async/awaitの学習コスト 非同期処理が主体
time.sleep ✅ 最もシンプル ❌ ブロッキング
❌ キャンセル不可
debounceには不適