存在重复元素 III 图解题解
这道题到底在问什么
- 输入
- nums=[10,4,7,1,9,3], indexDiff=2, valueDiff=2
- 输出
- true(7 与 9)
- 输入
- nums=[1,5,9,1,5,9], indexDiff=2, valueDiff=3
- 输出
- false
最优解:一步一步想明白
- 3记住「窗口管下标、有序集合管数值」,下面每帧都在套它。
- 4开扫前窗口是空的。指针从下标 0 起一位一位往右走,先在有序窗口里查有没有数值够近的邻居,没有就把自己加进去,窗口太长就把最老的挤掉。
- 5轮到下标 0,值是 10。先看它跟「有序窗口」里那批最近的数比,有没有谁跟它数值挨得够近。
- 6把「值差 ≤ 2」翻译成区间:只要窗口里有数落在 [8, 12] 之间,就算找到。问题变成「窗口里有没有数在这个区间里」。
- 7在有序窗口里找第一个不小于 8 的值:一个也没有。这一位找不到合格邻居,那就把它自己加进窗口,留给后面的数来比。
- 8把 10 按数值大小插到窗口里的正确位置,窗口始终保持有序,这样下次二分查找才快。现在窗口里是 { 10 }。
- 9轮到下标 1,值是 4。先看它跟「有序窗口」里那批最近的数比,有没有谁跟它数值挨得够近。
- 10把「值差 ≤ 2」翻译成区间:只要窗口里有数落在 [2, 6] 之间,就算找到。问题变成「窗口里有没有数在这个区间里」。
- 11在有序窗口里找第一个不小于 2 的值:是 10,可惜它大于 6、没落进区间。这一位找不到合格邻居,那就把它自己加进窗口,留给后面的数来比。
- 12把 4 按数值大小插到窗口里的正确位置,窗口始终保持有序,这样下次二分查找才快。现在窗口里是 { 4, 10 }。
- 13轮到下标 2,值是 7。先看它跟「有序窗口」里那批最近的数比,有没有谁跟它数值挨得够近。
- 14把「值差 ≤ 2」翻译成区间:只要窗口里有数落在 [5, 9] 之间,就算找到。问题变成「窗口里有没有数在这个区间里」。
- 15在有序窗口里找第一个不小于 5 的值:是 10,可惜它大于 9、没落进区间。这一位找不到合格邻居,那就把它自己加进窗口,留给后面的数来比。
- 16把 7 按数值大小插到窗口里的正确位置,窗口始终保持有序,这样下次二分查找才快。现在窗口里是 { 4, 7, 10 }。
- 17窗口只能装最近 2 个,现在多了一个,得把最老的那位、也就是下标 0 的 10 从有序窗口里删掉。这样窗口里永远只剩跟当前下标够近的数,「下标差 ≤ 2」这个条件就自动守住了。
- 18轮到下标 3,值是 1。先看它跟「有序窗口」里那批最近的数比,有没有谁跟它数值挨得够近。
- 19把「值差 ≤ 2」翻译成区间:只要窗口里有数落在 [-1, 3] 之间,就算找到。问题变成「窗口里有没有数在这个区间里」。
- 20在有序窗口里找第一个不小于 -1 的值:是 4,可惜它大于 3、没落进区间。这一位找不到合格邻居,那就把它自己加进窗口,留给后面的数来比。
- 21把 1 按数值大小插到窗口里的正确位置,窗口始终保持有序,这样下次二分查找才快。现在窗口里是 { 1, 4, 7 }。
- 22窗口只能装最近 2 个,现在多了一个,得把最老的那位、也就是下标 1 的 4 从有序窗口里删掉。这样窗口里永远只剩跟当前下标够近的数,「下标差 ≤ 2」这个条件就自动守住了。
- 23轮到下标 4,值是 9。先看它跟「有序窗口」里那批最近的数比,有没有谁跟它数值挨得够近。
- 24把「值差 ≤ 2」翻译成区间:只要窗口里有数落在 [7, 11] 之间,就算找到。问题变成「窗口里有没有数在这个区间里」。
- 25有序窗口里第一个不小于 7 的是 7,它正好 ≤ 11,落在区间里!这就找到了一对:下标 2 的 7 和下标 4 的 9,下标差和值差都达标,直接返回 true。
- 26回头看整趟:窗口始终只留最近 2 个数、且保持有序;每来一个数,只用一次「找 ≥ x−2 的第一个值」的查找就判完了「有没有够近的邻居」。下标用窗口管、数值用有序集合管,一遍线性扫加每步 O(log k) 查找,就把答案找了出来。
⚠️ 容易写错的地方
✗ 错:在整个数组里两两比对
✓ 对:只在「最近 indexDiff 个」的窗口里比
下标差要 ≤ indexDiff,窗口外的元素下标太远,不用比
✗ 错:逐个比窗口里每个数的值差
✓ 对:有序集合做区间查询,一次 O(log k)
排序后只需找第一个 ≥ x−valueDiff 的值看是否 ≤ x+valueDiff,不必遍历
✗ 错:滑动时忘删最老元素 / 用 int 相减
✓ 对:走过 indexDiff 就删 nums[i−indexDiff];值用 long
不删会把下标差超 indexDiff 的算进来;valueDiff 可达 1e9,int 相减会溢出
完整代码(Python / C++ / Java)
Python
from typing import List
from bisect import bisect_left, insort
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
window = []
for i, x in enumerate(nums):
j = bisect_left(window, x - valueDiff)
if j < len(window) and window[j] <= x + valueDiff:
return True
insort(window, x)
if i >= indexDiff:
old = nums[i - indexDiff]
window.pop(bisect_left(window, old))
return FalseC++
#include <set>
#include <vector>
using namespace std;
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int indexDiff, int valueDiff) {
multiset<long long> window;
for (int i = 0; i < (int)nums.size(); ++i) {
long long x = nums[i];
auto it = window.lower_bound(x - valueDiff);
if (it != window.end() && *it <= x + valueDiff) return true;
window.insert(x);
if (i >= indexDiff) window.erase(window.find(nums[i - indexDiff]));
}
return false;
}
};Java
import java.util.*;
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) {
TreeSet<Long> set = new TreeSet<>();
for (int i = 0; i < nums.length; i++) {
long x = nums[i];
Long y = set.ceiling(x - valueDiff);
if (y != null && y <= x + valueDiff) return true;
set.add(x);
if (i >= indexDiff) set.remove((long) nums[i - indexDiff]);
}
return false;
}
}复杂度
时间
O(n log k)
平衡树有序窗口(Java TreeSet / C++ multiset):每个元素查找/插入/删除各 O(log k)。Python 这版用 list+bisect,插入删除是 O(k)、整体 O(nk)
空间
O(k)
有序窗口最多装 k=indexDiff 个元素
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 存在重复元素 III 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能做到 O(n)?+
能,用「桶」分法:把数值按 valueDiff+1 的宽度分桶,同一个桶里必有合格对;相邻桶再比一下边界。桶数也用滑动窗口限制在 indexDiff 个。它把每步的 O(log k) 查找降成 O(1) 哈希,整体 O(n),代价是分桶、负数取整等细节更易写错。面试里有序集合写法最稳,桶法作为进阶优化。
有序集合为什么用 multiset / 允许重复?+
窗口里可能有相等的值(不同下标),Java 的 TreeSet 存的是去重后的值,但因为窗口只保留最近 k 个、且我们在加入新值前就先查过,通常不影响正确性;更稳妥的是 C++ multiset 或值带下标。关键是删除时按「最老下标对应的值」精确删一个,别把同值的都删了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 存在重复元素 III 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。