LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置” 目录一、推荐先做入门的 217二、哈希表里到底应该保存什么三、同一个数字出现很多次key 怎么办四、为什么要保存“最近一次出现的位置”五、为什么只保存“最近一次”就够了六、按照这个思路我一开始写出的代码七、这里其实不需要 abs()八、代码还能继续简化第一次出现时以前出现过但是距离太远时九、最后代码十、这道题和前面的哈希表题有什么关系LeetCode 219存在重复元素 II一、推荐先做入门的 217我之前做过 217「存在重复元素」。这道题算是入门的用于理解怎么使用哈希表中的set。链接是这个当时 217 只需要判断当前数字以前有没有出现过所以一个set就够了。例如seenset()每次扫描当前数字时以前见过 → True 以前没见过 → 加入 seen但 219 又多了一个条件两个相同元素的下标之差必须小于等于 k。也就是说这道题不只是问这个数字以前有没有出现过还要继续问它上一次出现在哪里所以只用set已经不够了。因为set只能告诉我有没有却不能告诉我在哪里。这时候就自然而然地想到了哈希表中的另外一种形式dict二、哈希表里到底应该保存什么既然题目最后要比较两个相同数字之间的下标距离那么字典最自然可以保存数字 → 下标例如1 → 3 2 → 5 4 → 7表示数字 1 出现在下标 3 数字 2 出现在下标 5 数字 4 出现在下标 7于是当我扫描到当前nums[i]时如果这个数字以前出现过就可以直接拿出之前保存的下标计算i-groups[num]然后判断 k就可以了。不过这里很容易出现一个新的问题如果同一个数字出现很多次那dict里的 key 不就重复了吗三、同一个数字出现很多次key 怎么办比如nums [1, ..., 1, ..., 1]数字1可能出现在下标 0 下标 5 下标 7那字典是不是要变成1 → 0 1 → 5 1 → 7其实不行。因为字典里的key是不能重复的。如果已经有groups[1]0后面又写groups[1]5并不会出现两个1而是会直接把原来的 value 覆盖掉原来1 → 0 重新赋值以后1 → 5也就是说相同 key 再次赋值时会更新这个 key 对应的 value。那这里新的问题就来了我们到底应不应该覆盖旧下标答案是应该。而且这正好是这道题真正关键的地方。四、为什么要保存“最近一次出现的位置”还是看刚才这个例子。数字1分别出现在下标 0 下标 5 下标 7假设k 3第一次扫描到下标01 → 0先记录下来。之后扫描到下标5。因为1已经在字典里面所以计算5 - 0 5大于3所以现在还不能返回True。那这时候要不要继续保留1 → 0如果一直保留下标0等之后扫描到下标7时7 - 0 7还是不满足。但实际上7 - 5 2已经满足 3所以在扫描到下标5时虽然这一次还不能返回True但应该把1 → 0更新成1 → 5这样后面来到下标7时比较的就是7 - 5 2于是就能正确找到答案。所以这里字典真正应该保存的并不是数字 → 第一次出现的位置而是数字 → 最近一次出现的位置五、为什么只保存“最近一次”就够了这里还可以继续想一步。假设当前下标是i同一个数字以前出现过很多次p1 p2 p3 ... i那么离当前i最近的一定是最后一次出现的位置也就是最大的那个下标。比如以前出现的位置 0 5 8 现在的位置10那么距离分别是10 - 0 10 10 - 5 5 10 - 8 2最近的一定是8所以如果连i - 最近一次出现的位置都已经大于k那么更早的位置只会离得更远更不可能满足要求。也就是说最近一次都不行 ↓ 更早的更不可能行因此对于每个数字只保留最近一次出现的位置就够了。而dict相同 key 重新赋值时会覆盖旧 value 的特性在这里反而正好符合我们的需求。每次都可以groups[num]i让它始终保存num → 最新下标六、按照这个思路我一开始写出的代码想清楚这一点以后我一开始写的是classSolution:defcontainsNearbyDuplicate(self,nums:list[int],k:int)-bool:groups{}fori,numinenumerate(nums):ifnumnotingroups:groups[num]ielse:ifabs(groups[num]-i)k:returnTrueelse:groups[num]ireturnFalse整个过程就是扫描当前数字 ↓ 以前没出现过 → 记录当前下标 以前出现过 ↓ 拿出之前保存的下标 ↓ 计算两个位置之间的距离 ↓ 距离 k → True 距离 k ↓ 旧位置已经没有继续保留的必要 ↓ 更新成当前位置这套逻辑本身是可以的。七、这里其实不需要 abs()我一开始写的是abs(groups[num]-i)但后来发现这里其实不需要abs()。因为我们一直按照从左到右扫描数组。以前保存的位置一定是在当前i的左边。也就是说groups[num] i所以i-groups[num]一定不会是负数。因此可以直接写i-groups[num]而不需要abs(groups[num]-i)八、代码还能继续简化再看一遍我原来的写法ifnumnotingroups:groups[num]ielse:ifgroups[num]和 i 的距离k:returnTrueelse:groups[num]i这里其实有重复。因为第一次出现时要做groups[num]i以前出现过但是距离太远时最后还是要做groups[num]i也就是说只要没有returnTrue当前下标最终都应该成为这个数字最近一次出现的位置所以groups[num]i完全可以统一放到最后。于是原来的没出现过 → 保存 出现过但距离太远 → 更新可以合并成只要这一次没有找到答案 → 一律把当前位置保存成最新位置这样代码就会简单很多。九、最后代码classSolution:defcontainsNearbyDuplicate(self,nums:list[int],k:int)-bool:groups{}fori,numinenumerate(nums):ifnumingroupsandi-groups[num]k:returnTruegroups[num]ireturnFalse整个过程现在可以压缩成扫描当前数字 ↓ 这个数字以前出现过 ↓ 有 → 看当前下标和最近一次出现位置的距离 ↓ 距离 k → True 否则 ↓ groups[num] i ↓ 把当前位置更新成这个数字最新的位置这里groups[num]i虽然只有一行但其实同时处理了两种情况第一次出现 → 新增一个 key-value 已经出现过 → 用当前下标覆盖旧 value最终groups始终保持数字 → 最近一次出现的下标十、这道题和前面的哈希表题有什么关系这道题让我进一步理解dict 的 value 不只是“随便保存一个下标”。具体保存什么要看题目真正需要什么信息。比如LC 242 有效的字母异位词 字符 → 出现次数LC 1 两数之和 数字 → 下标而这道 219 是数字 → 最近一次出现的下标这里“最近一次”非常重要。因为题目关心的是相同数字之间的最近距离而且这道题也让我更清楚dict中重复 key 的处理方式key 不会重复保存 ↓ 再次给同一个 key 赋值 ↓ 原来的 value 会被覆盖有些情况下“覆盖”可能意味着丢失信息。但在这道题里覆盖旧位置恰好就是我们需要做的事情因为旧位置没有最近一次的位置更有价值。