在问 · 算法题解

220. 存在重复元素 III

困难滑动窗口桶(按值分桶 + 滑动窗口)力扣原题 ↗

题目描述

输入整数数组 nums,以及两个整数 indexDiff 和 valueDiff。

判断是否存在一对下标 (i, j) 同时满足:i != j;两个下标的距离 abs(i - j) 不大于 indexDiff;两处数值之差 abs(nums[i] - nums[j]) 不大于 valueDiff。

存在这样的下标对时返回 true,否则返回 false。

示例 1
输入:nums = [1,2,3,1], indexDiff = 3, valueDiff = 0
输出:true

解释:取 (i, j) = (0, 3):下标相距 3,值都为 1,差为 0,三个条件都成立。

示例 2
输入:nums = [1,5,9,1,5,9], indexDiff = 2, valueDiff = 3
输出:false

解释:检查所有下标对,没有一对能同时满足三个条件。

数据范围

  • 2 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • 1 <= indexDiff <= nums.length
  • 0 <= valueDiff <= 10^9

解题思路:桶(按值分桶 + 滑动窗口)

下标限制用滑动窗口处理:只保留最近 indexDiff 个数。值限制用分桶处理:桶宽 w = valueDiff + 1,值 x 放进编号 x // w 的桶。同一个桶里的两个数之差一定 <= valueDiff,所以窗口中每个桶最多一个数;差值 <= valueDiff 的数只可能在同桶或相邻桶,检查 3 个桶即可。

为什么正确

处理 nums[i] 之前,buckets 恰好存着 nums[max(0, i-indexDiff) .. i-1],且每个桶至多一个数(否则早已返回 true)。

复杂度分析

时间 O(n),额外空间 O(min(n, indexDiff))。

易错点

负数分桶要用向下取整(Python 的 //,Java 的 Math.floorDiv)。C++/Go 的整数除法向 0 截断,-1 / 4 得 0,会让 -1 与 1 落进同一个桶而误判。桶宽是 valueDiff + 1,不是 valueDiff(valueDiff 可能为 0)。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
        w = valueDiff + 1
        buckets = {}
        for i, x in enumerate(nums):
            b = x // w
            if b in buckets:
                return True
            if b - 1 in buckets and x - buckets[b - 1] <= valueDiff:
                return True
            if b + 1 in buckets and buckets[b + 1] - x <= valueDiff:
                return True
            buckets[b] = x
            if i >= indexDiff:
                del buckets[nums[i - indexDiff] // w]
        return False

载入编辑器,自己跑一遍 →

其他解法

有序集合按值排序,用一次 lower_bound 就能找到窗口内离 x 最近的较大值;桶解法把值域切成宽 valueDiff+1 的桶,同桶必满足、只需再检查相邻桶,把查找降到 O(1)。

解法 2:滑动窗口 + 有序集合 时间 O(n log k),空间 O(min(n, k))

维护一个有序集合,里面恰好是当前下标 i 之前 indexDiff 个元素。对每个 x = nums[i],在集合中找第一个 ≥ x - valueDiff 的元素,如果它存在且 ≤ x + valueDiff,就找到了答案。然后把 x 加入集合;当 i ≥ indexDiff 时,把已经离开窗口的 nums[i - indexDiff] 移除。C++ 用 multiset、Java 用 TreeMap 计数;Python、Go、JS/TS 没有内置的有序集合,用有序数组 + 二分查找代替。

思路直接:只要求窗口内“离 x 最近”的值,不需要分桶的巧思。多一个 log 因子;用有序数组代替时插入和删除要移动元素,最坏 O(k)。

from bisect import bisect_left, insort
from typing import List

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
        window = []
        for i, x in enumerate(nums):
            p = bisect_left(window, x - valueDiff)
            if p < len(window) and window[p] <= x + valueDiff:
                return True
            insort(window, x)
            if i >= indexDiff:
                window.pop(bisect_left(window, nums[i - indexDiff]))
        return False

滑动窗口 专题的其他题目

查看 滑动窗口 全部题目 →