题目描述

✅ LCR 057. 存在重复元素 III

image-20260929010235543

image-20260929010235544

题意分析

寻找两个不同下标,使下标距离不超过 k,同时数值差的绝对值不超过 t。一个条件约束位置,另一个条件约束值,必须同时满足。

处理当前值 x = nums[i] 时,只需查询前面至多 k 个元素中,是否存在落在 [x-t, x+t] 内的值。这样可以把下标条件交给滑动窗口,把数值条件交给窗口中的查询结构。

解法:滑动窗口(有序集合与分桶)

核心思路

[!blue]

查询下标 i 时,容器保存的历史下标范围是 [max(0, i-k), i-1]。先查询,再插入当前值;随后若 i >= k,删除下标 i-k 的旧值,为下一轮准备窗口。这个旧值与当前下标相距恰好 k,本轮仍然合法,所以必须在本轮查询之后才删除。

Java 用有序集合回答数值查询。取 ceiling(x-t),即不小于下界的最小值:若它存在且不超过 x+t,就找到合法配对;若它已经超过上界,集合中更大的值也不可能符合要求。上下界在 64 位整数中计算,避免极端值的加减溢出。

这里使用集合而非多重集合仍然正确:在题目 t >= 0 的条件下,窗口内如果出现两个相等值,查询第二个时已经返回 true。因此继续执行插入和删除的路径上,窗口里不会同时保留重复值,不会因为按值删除而误删其他副本。

Go 把值域按宽度 size = t+1 划成桶。每个桶包含连续的 t+1 个整数,同桶内两值的最大差为 t,所以同桶已有元素时可以立即返回。相隔至少两个桶的值差必然大于 t,其余只需检查左右相邻桶,并比较真实差值。

桶号必须向下取整,让负数也按相同宽度分组。非负数直接除以 size;负数使用 (value+1)/size-1,修正整数除法向零截断的行为。桶宽、桶号和差值都使用 int64。

同桶命中会直接结束搜索,所以每个桶只需保存一个值。两种实现都在插入前查询,保证当前元素不会和自己配对;容器只保留窗口范围,又保证了下标距离。

解题步骤

  1. 从左向右遍历,先在此前至多 k 个元素中查找数值足够接近的候选。
  2. Java 查询数值下界的后继;Go 检查当前桶和两个相邻桶,命中就返回 true。
  3. 未命中时插入当前值,若 i >= k,再删除本轮使用完的 nums[i-k]。
  4. 全部元素处理完仍未命中则返回 false。k = 0 时没有不同下标可选;t = 0 时数值条件退化为完全相等。

代码实现

class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        TreeSet<Long> ts = new TreeSet<>();

        for (int i = 0; i < nums.length; ++i) {
            Long x = ts.ceiling((long) nums[i] - (long) t);

            if (x != null && x <= (long) nums[i] + (long) t) {
                return true;
            }

            ts.add((long) nums[i]);

            if (i >= k) {
                ts.remove((long) nums[i - k]);
            }
        }

        return false;
    }
}
func containsNearbyAlmostDuplicate(nums []int, k int, t int) bool {
    if k <= 0 || t < 0 {
        return false
    }
    size := int64(t) + 1
    buckets := map[int64]int64{}
    for i, num := range nums {
        value := int64(num)
        id := bucketID(value, size)
        if _, ok := buckets[id]; ok {
            return true
        }
        if v, ok := buckets[id-1]; ok && value-v <= int64(t) {
            return true
        }
        if v, ok := buckets[id+1]; ok && v-value <= int64(t) {
            return true
        }
        buckets[id] = value
        if i >= k {
            delete(buckets, bucketID(int64(nums[i-k]), size))
        }
    }
    return false
}

func bucketID(value int64, size int64) int64 {
    if value >= 0 {
        return value / size
    }
    return (value+1)/size - 1
}

复杂度分析

设数组长度为 n,w = min(n, k+1),包含插入当前值后、删除旧值前的短暂容量。

  • 时间复杂度:Java 为 $O(n\log(w+1))$,每项进行常数次有序集合操作;Go 为期望 $O(n)$,每项只检查三个桶并做常数次哈希操作。
  • 空间复杂度:$O(w)$,容器只保存当前窗口及刚插入的一项;每个桶至多保留一个值。

关键点总结

[!green]

  • 窗口负责下标距离,范围查询或分桶负责数值距离。
  • 下标 i-k 本轮仍可参与配对,查完当前元素后才为下一轮移除。
  • 有序集合只需检查区间下界的最小后继;分桶只需检查同桶和相邻桶。
  • 正确处理负数桶号和 64 位边界运算,是分桶判定成立的前提。

易错点总结

[!yellow]

  • 先插入当前值再查询:会把当前元素当作自己的配对对象。
  • 查询前就删除下标 i-k:会漏掉下标距离恰好等于 k 的合法配对。
  • 只查同桶:两个足够接近的值也可能跨过桶边界,必须检查相邻桶。
  • 相邻桶命中就直接返回:相邻不代表差值一定足够小,仍需比较真实差值。
  • 负数直接按向零截断计算桶号:会破坏桶的统一宽度,可能让差值过大的数误落在同桶。
  • 先以 32 位计算上下界再转型:溢出可能已经发生,应先提升类型再运算。

相似题目

题目 难度 关联与区别
219. 存在重复元素 II 简单 都限制下标距离,原题只查完全相同的值,本题允许值差,需要有序窗口或桶定位。
480. 滑动窗口中位数 困难 同样维护会插入和过期删除的窗口,原题查询中位数,本题查询邻近数值范围。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/75684937
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!