这个哈希表的坑,让我差点道心破碎

92 阅读7分钟

力扣2841:这个哈希表的坑,让我差点道心破碎!

作者:一个被哈希表折磨了一夜的普通程序员

前言

朋友们,今天给大家带来力扣第2841题的解析。这道题看似简单,实则暗藏玄机,尤其是那个关于哈希表的细节,让我昨天想了一整夜都没想明白。直到今天早上灵光一闪,才终于弄懂了其中的门道。

题目分析

屏幕截图 2025-12-08 222105.png

先来看看题目要求:找到几乎唯一的子数组。什么是“几乎唯一”?就是说在长度为k的子数组中,至少有m个不同的元素。

看到“子数组”这三个字,我们第一反应就是滑动窗口;看到“不同元素个数”,我们自然会想到哈希表。思路很清晰对不对?我也是这么想的,但没想到还是掉坑里了。

我的第一版思路

我一开始的思路很简单:

  1. 用滑动窗口遍历所有长度为k的子数组
  2. 用哈希表统计窗口内不同元素的个数
  3. 如果不同元素个数≥m,就记录当前子数组的和
  4. 在所有符合条件的子数组中找最大值

代码大概是这样的:

let map = new Map();
let sum = 0;
let maxSum = 0;

for(let i = 0; i < nums.length; i++) {
    if(i <= k - 1) {
        // 填充初始窗口
        map.set(nums[i]);
        sum += nums[i];
    } else {
        // 检查当前窗口是否符合条件
        if(map.size >= m) {
            maxSum = Math.max(maxSum, sum);
        }
        
        // 滑动窗口:移除左边元素,添加右边元素
        map.set(nums[i]);
        map.delete(nums[i - k]);
        sum += nums[i];
        sum -= nums[i - k];
    }
}

看起来逻辑挺清晰的,对不对?但就是这个看起来没问题的代码,让我栽了个大跟头。

掉坑记录

让我们用题目给的例子来测试一下:

屏幕截图 2025-12-08 223915.png

nums = [1, 4, 1, 4]
m = 2, k = 3

按照我们的算法:

  1. 第一个窗口:[1, 4, 1] → 哈希表:{1, 4},size=2,符合条件
  2. 滑动窗口:移除最左边的1,添加右边的4
  3. 第二个窗口:[4, 1, 4] → 哈希表应该是什么?

问题就出在这里!当我们执行map.delete(1)时,哈希表中所有的1都被删掉了!但是我们的窗口里明明还有一个1啊!

所以实际情况是:

  • 移除1后,哈希表变成:{4}
  • 添加4后,哈希表还是:{4}
  • 最终size=1,不符合条件,但实际上窗口[4, 1, 4]中有两个不同的元素(1和4)!

这就是问题的核心:我们误以为哈希表的每个键只出现一次,但实际上窗口内可能有多个相同的值

挣扎与尝试

发现问题后,我第一个想法是:既然直接删除不行,那我每个窗口都重新计数总可以吧?

for(let i = 0; i < nums.length; i++) {
    // ... 其他代码
    
    if(i > k - 1) {
        // 清空哈希表,重新统计当前窗口
        map.clear();
        for(let j = i; j > i - k; j--) {
            map.set(nums[j]);
        }
        // ... 其他逻辑
    }
}

这个方法确实能解决准确性的问题,但带来了新的问题:时间复杂度爆炸

每个窗口都要重新遍历k个元素,总时间复杂度变成了O(n*k)。对于大数据量的测试用例,直接超时!

屏幕截图 2025-12-08 224959.png

离成功只差一步,却因为性能问题被卡住,这种感觉谁懂啊!😭

灵光一闪:计数哈希表

在经历了无数次的尝试和失败后,我终于想明白了:我们需要的不只是记录元素是否出现,还要记录元素出现的次数

也就是说,我们需要一个计数哈希表,而不是简单的集合。

正确的思路应该是:

  1. 用哈希表记录每个元素在窗口内出现的次数
  2. 添加元素时,计数器+1
  3. 移除元素时,计数器-1
  4. 只有当计数器减到0时,才从哈希表中删除这个键

这样就能准确反映窗口内不同元素的真实情况了!

最终的正确代码

var maxSum = function(nums, m, k) {
    let cnt = new Map();  // 计数哈希表
    let s = 0;  // 当前窗口的和
    let ans = 0;
    
    for(let i = 0; i < nums.length; i++) {
        // 添加右边元素
        s += nums[i];
        cnt.set(nums[i], (cnt.get(nums[i]) || 0) + 1);
        
        let left = i - k + 1;  // 窗口左边界
        if(left < 0) continue;  // 窗口还没填满
        
        // 检查当前窗口是否符合条件
        if(cnt.size >= m) {
            ans = Math.max(ans, s);
        }
        
        // 移除左边元素(准备滑动窗口)
        const out = nums[left];
        s -= out;
        const c = cnt.get(out);
        
        // 核心逻辑:只有计数为1时才删除键
        if(c > 1) {
            cnt.set(out, c - 1);
        } else {
            cnt.delete(out);
        }
    }
    
    return ans;
};

让我们用一个完整的例子来理解这个解法!

题目例子

假设我们有:

  • nums = [1, 4, 1, 4, 2]
  • m = 2(至少需要2个不同元素)
  • k = 3(子数组长度为3)

逐步执行过程

第1步:i=0 (nums[0]=1)

窗口: [1]
s = 1
cnt = {1: 1}
left = 0-3+1 = -2 < 0, 跳过检查

第2步:i=1 (nums[1]=4)

窗口: [1, 4]
s = 1 + 4 = 5
cnt = {1: 1, 4: 1}
left = 1-3+1 = -1 < 0, 跳过检查

第3步:i=2 (nums[2]=1)

窗口: [1, 4, 1] ✓ 窗口填满了!
s = 5 + 1 = 6
cnt = {1: 2, 4: 1}
left = 2-3+1 = 0 >= 0

检查条件:cnt.size = 2 >= m=2ans = max(0, 6) = 6

准备移除左边元素:out = nums[0] = 1
s = 6 - 1 = 5
c = cnt.get(1) = 2

因为c=2>1,所以只减少计数:
cnt = {1: 1, 4: 1}

第4步:i=3 (nums[3]=4)

当前窗口: [4, 1, 4](即将形成)
s = 5 + 4 = 9
cnt = {1: 1, 4: 2}
left = 3-3+1 = 1

检查条件:cnt.size = 2 >= m=2ans = max(6, 9) = 9

准备移除左边元素:out = nums[1] = 4
s = 9 - 4 = 5
c = cnt.get(4) = 2

因为c=2>1,所以只减少计数:
cnt = {1: 1, 4: 1}

第5步:i=4 (nums[4]=2)

当前窗口: [1, 4, 2](即将形成)
s = 5 + 2 = 7
cnt = {1: 1, 4: 1, 2: 1}
left = 4-3+1 = 2

检查条件:cnt.size = 3 >= m=2ans = max(9, 7) = 9

准备移除左边元素:out = nums[2] = 1
s = 7 - 1 = 6
c = cnt.get(1) = 1

因为c=1,所以删除键:
cnt = {4: 1, 2: 1}

最终结果

ans = 9

这个结果对应的是窗口[4, 1, 4](和为9),它包含两个不同元素(1和4)。

另一个有趣的例子

让我们看看如果所有元素都相同会发生什么:

nums = [5, 5, 5, 5]
m = 2, k = 3

执行过程:

第1-3步:构建初始窗口[5, 5, 5]

s = 15
cnt = {5: 3}
cnt.size = 1 < 2,不符合条件

第4步:滑动到[5, 5, 5]

窗口内只有元素5,cnt.size始终为1
永远不会满足cnt.size >= 2的条件
所以ans保持为0

为什么计数哈希表如此重要?

让我们回到最初的问题:[1, 4, 1, 4], m=2, k=3

如果用简单的集合(delete直接删除键):

窗口[1,4,1] → 集合{1,4} ✓
移除1,添加4 → 集合{4} ✗(丢失了第二个1

如果用计数哈希表:

窗口[1,4,1] → 哈希表{1:2, 4:1} ✓
移除一个1(计数从2减到1) → 哈希表{1:1, 4:1} ✓
添加4 → 哈希表{1:1, 4:2} ✓
size始终是2

算法时间复杂度分析

这个算法的时间复杂度是O(n),其中n是数组长度,因为:

  1. 每个元素被添加一次(O(1))
  2. 每个元素被移除一次(O(1))
  3. 每次操作哈希表的get/set都是O(1)

空间复杂度是O(k),因为哈希表最多存储k个键值对。

这就是为什么这个解法既正确又高效的原因!🎉

关键点总结

  1. 计数哈希表 vs 普通集合:当元素可能重复出现时,必须使用计数哈希表
  2. 正确的删除时机:只有当元素计数减到0时,才从哈希表中删除
  3. 时间复杂度:O(n),每个元素只被添加和删除一次
  4. 空间复杂度:O(k),最多存储k个不同的元素

类似题目推荐

如果你理解了这道题,可以尝试一下力扣第2461题《长度为k子数组的最大和》,解题思路几乎一模一样,都是滑动窗口+计数哈希表的组合。

后记

这道题让我深刻理解了一个道理:在编程中,细节决定成败。一个看似简单的删除操作,背后可能隐藏着复杂逻辑。不过也正是这种挑战,让编程变得有趣,不是吗?

希望我的踩坑经历能帮助你少走弯路。如果你也有类似的经历,欢迎在评论区分享!