LeetCode简单难度的经典题目「存在重复元素 II」,这道题看似简单,但想要做到时间和空间的最优平衡,还是有一些小技巧的。题目本身是数组判重的延伸,核心考点是「哈希表的高效运用」,适合新手入门哈希表的实际场景,也能帮大家巩固数组遍历的优化思路。
一、题目解读:读懂需求,找对方向
先明确题目要求,避免踩坑:
给定一个整数数组 nums 和一个整数 k,判断数组中是否存在 两个不同的索引 i 和 j,满足两个条件:
-
nums[i] == nums[j](两个元素值相等);
-
abs(i - j) ≤ k(两个元素的索引差的绝对值不超过k)。
如果同时满足这两个条件,返回 true;否则返回 false。
关键易错点:「不同的索引」—— 也就是说,同一个元素(i=j)不算;索引差是「绝对值」,i可以比j大,也可以比j小,只要差值不超过k即可。
举两个简单例子,帮大家快速理解:
-
示例1:nums = [1,2,3,1],k = 3 → 返回true(索引0和3的元素都是1,差值3≤3);
-
示例2:nums = [1,0,1,1],k = 1 → 返回true(索引2和3的元素都是1,差值1≤1);
-
示例3:nums = [1,2,3,1,2,3],k = 2 → 返回false(所有相等元素的索引差都大于2)。
二、初始思路:暴力解法(不可取,仅作对比)
拿到题目,最直观的思路就是「双重循环」:遍历每个元素nums[i],再遍历它后面k个元素(或者所有元素),判断是否有相等且索引差≤k的元素。
暴力解法代码(仅参考,不推荐):
function containsNearbyDuplicate(nums: number[], k: number): boolean {
for (let i = 0; i < nums.length; i++) {
// 只需要遍历i后面k个元素,超过k的索引差一定不满足,减少循环次数
for (let j = i + 1; j <= i + k && j < nums.length; j++) {
if (nums[i] === nums[j]) {
return true;
}
}
}
return false;
};
暴力解法的问题很明显:
-
时间复杂度:O(nk) —— 最坏情况下,每个元素都要遍历后面k个元素,当k接近n时,复杂度接近O(n²),数据量大时会超时(LeetCode测试用例中,n可以达到10⁵,O(n²)绝对超时);
-
空间复杂度:O(1) —— 不需要额外空间,但时间效率太低,得不偿失。
所以,我们需要找到一种「时间更优」的解法,牺牲少量空间来换取时间效率,这也是算法题中常见的「空间换时间」思路。
三、优化解法:哈希表(时间O(n),空间O(n),最优平衡)
核心思路:用哈希表(Map)存储「元素值 → 最近一次出现的索引」,遍历数组时,对于每个元素nums[i],做3件事:
-
判断哈希表中是否存在当前元素nums[i];
-
如果存在,计算当前索引i与哈希表中存储的索引(最近一次出现的位置)的差值,若差值≤k,直接返回true;
-
如果不存在,或者差值>k,就更新哈希表中当前元素对应的索引为i(重点:只存最近一次出现的索引,因为更远的索引和当前i的差值会更大,不可能满足≤k,存最近的能最大化后续匹配的可能性)。
优化代码(题目给出的最优解)
function containsNearbyDuplicate(nums: number[], k: number): boolean {
const map = new Map<number, number>();
for (let i = 0; i < nums.length; i++) {
if (map.has(nums[i])) {
// 存在该元素,判断索引差
if (i - map.get(nums[i])! <= k) {
return true;
}
}
// 不存在,或索引差不满足,更新索引(覆盖旧索引)
map.set(nums[i], i);
}
// 遍历完所有元素,未找到满足条件的 pair
return false;
};
代码逐行解析(新手必看)
-
创建Map:
const map = new Map<number, number>(),key存数组元素值,value存该元素最近一次出现的索引; -
遍历数组:
for (let i = 0; i < nums.length; i++),遍历一次即可,时间复杂度由这一步决定,是O(n); -
判断元素是否存在:
if (map.has(nums[i])),Map的has方法是O(1)时间复杂度,比数组查找高效得多; -
判断索引差:
i - map.get(nums[i])! <= k,这里的!是TypeScript的非空断言,因为我们已经用has判断过存在该key,所以get一定能拿到值,避免类型报错; -
更新索引:
map.set(nums[i], i),无论元素是否存在(存在但索引差不满足时,也要更新),都要存当前索引——因为旧索引距离后续元素更远,后续元素若和当前元素相等,用当前索引计算差值更小,更可能满足条件。
为什么这是最优解法?
我们来分析时间和空间复杂度:
-
时间复杂度:O(n) —— 只遍历一次数组,每个步骤(Map的has、get、set)都是O(1),整体线性时间,即使n达到10⁵也能轻松通过;
-
空间复杂度:O(n) —— 最坏情况下,数组中所有元素都不重复,Map会存储所有元素,空间开销和数组长度一致。但这是「空间换时间」的合理牺牲,相比暴力解法的超时,这种牺牲是必要的。
优化细节补充:为什么不存所有出现过的索引?比如用数组存某个元素的所有索引,再判断是否有两个索引差≤k。这样做的话,空间复杂度还是O(n),但判断索引差时可能需要多一次遍历,不如「只存最近一次索引」高效——因为最近的索引和当前索引的差值最小,只要这个差值不满足,更远的索引一定也不满足。
四、边界情况测试(避坑关键)
很多时候,代码能通过常规测试用例,但会栽在边界情况上,这道题的常见边界情况的如下,大家可以用这些用例测试自己的代码:
-
边界1:数组长度为1 → nums = [1],k = 0 → 返回false(只有一个元素,没有两个不同索引);
-
边界2:k = 0 → 此时要求i = j,但题目要求不同索引,所以无论数组如何,都返回false(比如nums = [1,1],k=0 → false);
-
边界3:所有元素都相同 → nums = [2,2,2,2],k = 2 → 返回true(任意两个相邻元素的索引差都是1≤2);
-
边界4:元素重复但索引差刚好等于k → nums = [1,3,5,1],k = 3 → 返回true(索引0和3,差值3);
-
边界5:元素重复但索引差大于k → nums = [1,2,1],k = 1 → 返回false(索引0和2,差值2>1)。
五、总结:解题思路提炼
这道题的核心是「用哈希表记录元素最近一次出现的位置」,核心逻辑可以总结为:
遍历数组 → 查哈希表,看当前元素是否出现过 → 出现过则判索引差,满足则返回true → 不满足或未出现,更新哈希表 → 遍历结束未找到,返回false。
对于新手来说,这道题是理解「空间换时间」思路的绝佳例子——暴力解法虽然空间最优,但时间效率太低,无法应对大数据量;而哈希表解法用O(n)的空间,换来了O(n)的时间,是工程中的最优选择(工程中更看重时间效率,只要空间开销在合理范围内)。
另外,这道题和「存在重复元素 I」(LeetCode 217)有很强的关联性:217题只需要判断是否有重复元素,用Set即可;219题多了「索引差≤k」的限制,所以需要用Map存储索引,记录最近一次出现的位置,才能高效判断。