markdown复制## 1. 题目解析与解题思路
LeetCode 219题"存在重复元素 II"是一道经典的数组处理题目,要求判断数组中是否存在两个相同的元素,且它们的下标差不超过给定的k值。这道题在面试中经常出现,因为它很好地考察了对哈希表这种数据结构的理解和应用能力。
题目具体要求:给定一个整数数组nums和一个整数k,如果数组中存在两个不同的索引i和j,使得nums[i] == nums[j]且abs(i - j) <= k,则返回true;否则返回false。
### 1.1 暴力解法分析
最直观的解法是使用双重循环遍历所有可能的元素对:
```c
for (int i = 0; i < numsSize; i++) {
for (int j = i + 1; j <= i + k && j < numsSize; j++) {
if (nums[i] == nums[j]) {
return true;
}
}
}
return false;
这种解法的时间复杂度是O(n*k),当k接近n时会退化为O(n²),在LeetCode上会超时。我们需要寻找更优的解法。
哈希表(散列表)可以在O(1)时间内完成元素的查找操作。我们可以维护一个哈希表,存储元素值到其最近出现位置的映射。遍历数组时,对于每个元素:
这种解法的时间复杂度是O(n),空间复杂度也是O(n),是典型的空间换时间策略。
C语言没有内置的哈希表实现,我们需要自己设计。对于这个问题,可以使用开放寻址法的简单哈希表:
c复制#define HASH_SIZE 10000
typedef struct {
int key;
int value;
} HashNode;
HashNode hashTable[HASH_SIZE];
我们使用简单的取模法作为哈希函数,线性探测解决冲突:
c复制int hash(int key) {
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
void insert(int key, int value) {
int idx = hash(key);
while (hashTable[idx].key != 0 && hashTable[idx].key != key) {
idx = (idx + 1) % HASH_SIZE;
}
hashTable[idx].key = key;
hashTable[idx].value = value;
}
int get(int key) {
int idx = hash(key);
while (hashTable[idx].key != 0) {
if (hashTable[idx].key == key) {
return hashTable[idx].value;
}
idx = (idx + 1) % HASH_SIZE;
}
return -1; // 表示未找到
}
c复制#include <stdbool.h>
#include <stdlib.h>
#define HASH_SIZE 10000
typedef struct {
int key;
int value;
} HashNode;
HashNode hashTable[HASH_SIZE];
int hash(int key) {
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
void insert(int key, int value) {
int idx = hash(key);
while (hashTable[idx].key != 0 && hashTable[idx].key != key) {
idx = (idx + 1) % HASH_SIZE;
}
hashTable[idx].key = key;
hashTable[idx].value = value;
}
int get(int key) {
int idx = hash(key);
while (hashTable[idx].key != 0) {
if (hashTable[idx].key == key) {
return hashTable[idx].value;
}
idx = (idx + 1) % HASH_SIZE;
}
return -1;
}
bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
// 清空哈希表
for (int i = 0; i < HASH_SIZE; i++) {
hashTable[i].key = 0;
hashTable[i].value = 0;
}
for (int i = 0; i < numsSize; i++) {
int prevPos = get(nums[i]);
if (prevPos != -1 && i - prevPos <= k) {
return true;
}
insert(nums[i], i);
}
return false;
}
哈希表大小HASH_SIZE的选择很重要:
对于LeetCode的测试用例,10000是一个合理的选择。在实际工程中,可能需要动态调整哈希表大小。
注意哈希函数中对负数的处理:
c复制return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
这种写法确保无论key是正是负,都能得到合法的数组下标。
当k远小于n时,可以维护一个大小为k+1的滑动窗口,使用哈希集合存储窗口内的元素:
c复制bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
if (k == 0) return false;
int left = 0;
for (int right = 0; right < numsSize; right++) {
if (right - left > k) {
// 从哈希表中移除nums[left]
left++;
}
// 检查nums[right]是否在哈希表中
// 如果在则返回true
// 否则添加到哈希表
}
return false;
}
这种实现的空间复杂度可以优化到O(k),但C语言实现起来更复杂。
基础用例:
无重复用例:
边界用例:
c复制void printHashTable() {
for (int i = 0; i < 10; i++) { // 只打印前10个
printf("[%d] key=%d, value=%d\n", i, hashTable[i].key, hashTable[i].value);
}
}
检查哈希冲突:
记录哈希表的总查询次数和冲突次数,评估哈希函数效率。
内存检查:
确保哈希表访问不会越界,特别是在处理负数时。
哈希表解法:
暴力解法:
哈希表解法:
滑动窗口优化:
如果数组很大但重复元素很少,如何优化空间?
如果k值很大但内存有限?
分布式环境下如何解决?
这类算法在实际工程中有广泛应用场景:
在实现生产级代码时,还需要考虑:
提示:在面试中遇到这类问题时,建议先阐述暴力解法,然后逐步优化,展示思考过程比直接给出最优解更重要。
哈希表未初始化:
负数处理不当:
边界条件遗漏:
哈希冲突过多:
下标计算错误:
对于追求极致性能的场景,可以尝试以下优化:
c复制#define GET_HASH(key) ((key % HASH_SIZE + HASH_SIZE) % HASH_SIZE)
使用更快的哈希算法:
如MurmurHash或CityHash,但要考虑实现复杂度
减少条件判断:
重构代码逻辑减少分支预测失败
循环展开:
对关键循环进行手动展开
使用位运算替代取模:
当HASH_SIZE是2的幂时,可以用&代替%
c复制#define HASH_SIZE 16384 // 2^14
#define GET_HASH(key) ((key) & (HASH_SIZE-1))
虽然本文聚焦C语言实现,但了解其他语言的实现方式也有助于深入理解:
python复制def containsNearbyDuplicate(nums, k):
seen = {}
for i, num in enumerate(nums):
if num in seen and i - seen[num] <= k:
return True
seen[num] = i
return False
java复制public boolean containsNearbyDuplicate(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i]) && i - map.get(nums[i]) <= k) {
return true;
}
map.put(nums[i], i);
}
return false;
}
cpp复制bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
if (map.count(nums[i]) && i - map[nums[i]] <= k) {
return true;
}
map[nums[i]] = i;
}
return false;
}
对比可见,高级语言借助内置的哈希表实现,代码更加简洁,但C语言实现能让我们更深入理解底层原理。
掌握了基础解法后,可以尝试以下变种问题:
存在重复元素III:
要求值差不超过t且下标差不超过k,需要结合滑动窗口和有序数据结构
统计所有满足条件的对数:
不仅要判断是否存在,还要统计所有满足i<j且nums[i]==nums[j]且j-i≤k的对数
流数据版本:
数据以流的形式到达,无法存储全部历史数据,如何解决?
分布式版本:
数据分布在多台机器上,如何分布式检测重复?
多维扩展:
对于二维数组或多维数据,如何定义"附近"的重复?
每个变种问题都需要在基础解法上进行创新和调整,是很好的面试进阶题目。
在实际编码和面试中,这类题目有几个关键点需要注意:
先理解题意:
明确"重复元素"和"下标差"的具体定义,画几个例子辅助理解
从暴力解法开始:
即使知道更优解,也应该先提出暴力解法,展示解题思路
分析复杂度:
明确说明暴力解法的问题,引出优化方向
数据结构选择:
解释为什么选择哈希表,其他数据结构为什么不合适
边界条件:
特别注意k=0、空数组、所有元素相同等特殊情况
代码风格:
即使是在白板编码,也要保持清晰的变量命名和适当的注释
测试验证:
写完代码后,用几个测试用例手动验证,展示严谨性
在LeetCode练习时,建议:
最后,这类数组+哈希表的问题在面试中非常常见,掌握其核心思想可以举一反三解决许多类似问题。在实际工程中,理解数据结构的底层实现比单纯会用API更重要,这也是为什么用C语言实现这类问题特别有价值。
code复制