1. 题目解析与解题思路
LeetCode 219题"存在重复元素II"是一道经典的数组处理题目,题目要求我们判断一个整数数组中是否存在两个相同的元素,且它们的下标差不超过给定的k值。这道题很好地考察了对数组遍历、哈希表应用和滑动窗口技巧的理解。
1.1 题目具体要求
给定一个整数数组nums和一个整数k,如果数组中有两个不同的索引i和j,满足nums[i] == nums[j]且abs(i - j) <= k,则返回true;否则返回false。
例如:
- 输入:nums = [1,2,3,1], k = 3
- 输出:true
- 解释:索引0和3的元素都是1,且3-0 <= 3
1.2 核心解题思路分析
解决这个问题主要有两种主流方法:
-
暴力解法:双重循环遍历所有元素对,检查是否满足条件。这种方法时间复杂度为O(n²),在LeetCode上会超时,不推荐使用。
-
哈希表法:维护一个哈希表,记录元素值到其最近出现位置的映射。遍历数组时,检查当前元素是否在哈希表中,且与上次出现位置的距离是否≤k。时间复杂度O(n),空间复杂度O(n)。
-
滑动窗口法:维护一个大小为k+1的滑动窗口,检查窗口内是否有重复元素。时间复杂度O(nk),空间复杂度O(k)。
对于C语言实现,哈希表法是最优选择,因为:
- C标准库没有内置哈希表,但可以自己实现简单的哈希表
- 相比滑动窗口法,哈希表法时间复杂度更低
- 适合处理大规模数据
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C语言实现详解
2.1 数据结构设计
在C语言中,我们需要自己实现哈希表。这里采用开放寻址法的简单哈希表实现:
c复制#define HASH_SIZE 10000
typedef struct {
int key; // 存储数组元素值
int value; // 存储该元素最后一次出现的索引
} HashItem;
HashItem hashTable[HASH_SIZE];
2.2 哈希函数设计
使用简单的取模哈希函数:
c复制int hash(int key) {
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
注意处理负数情况,通过+HASH_SIZE再取模确保结果为正。
2.3 哈希表操作实现
实现哈希表的插入和查找操作:
c复制// 向哈希表中插入键值对
void hashInsert(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 hashGet(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; // 未找到返回-1
}
2.4 主函数实现
基于上述数据结构,实现题目要求的函数:
c复制bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
memset(hashTable, 0, sizeof(hashTable)); // 初始化哈希表
for (int i = 0; i < numsSize; i++) {
int lastPos = hashGet(nums[i]);
if (lastPos != -1 && i - lastPos <= k) {
return true;
}
hashInsert(nums[i], i);
}
return false;
}
3. 代码优化与边界处理
3.1 哈希冲突处理优化
上述简单哈希表实现可能在数据量大时性能下降。我们可以做以下优化:
- 增大哈希表大小,减少冲突概率
- 使用更好的哈希函数,如乘法哈希
- 实现更复杂的冲突解决策略,如链地址法
优化后的哈希函数示例:
c复制int hash(int key) {
key = ((key >> 16) ^ key) * 0x45d9f3b;
key = ((key >> 16) ^ key) * 0x45d9f3b;
key = (key >> 16) ^ key;
return key % HASH_SIZE;
}
3.2 边界条件处理
需要特别注意以下边界情况:
- 空数组输入
- k=0的情况
- 数组中存在INT_MIN/INT_MAX等极值
- 大量重复元素的情况
修改后的主函数增加边界检查:
c复制bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
if (numsSize <= 1 || k <= 0) return false;
memset(hashTable, 0, sizeof(hashTable));
for (int i = 0; i < numsSize; i++) {
int lastPos = hashGet(nums[i]);
if (lastPos != -1 && i - lastPos <= k) {
return true;
}
hashInsert(nums[i], i);
}
return false;
}
4. 算法复杂度分析
4.1 时间复杂度
哈希表解法的时间复杂度主要取决于:
- 数组遍历:O(n)
- 哈希表操作:平均O(1),最坏O(n)
因此平均时间复杂度为O(n),最坏情况下为O(n²)。但在合理设计哈希函数和解决冲突策略后,实际表现接近O(n)。
4.2 空间复杂度
哈希表需要存储最多n个元素的信息,因此空间复杂度为O(n)。
4.3 与滑动窗口法的对比
滑动窗口法实现:
c复制bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
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(nk),空间复杂度O(1)。当k接近n时,退化为O(n²)。
因此,哈希表法在大多数情况下更优,特别是当k较大时。
5. 测试用例设计
全面的测试用例应包含以下情况:
-
基本用例:
- 输入:[1,2,3,1], k=3 → 输出:true
- 输入:[1,0,1,1], k=1 → 输出:true
-
边界用例:
- 输入:[1], k=0 → 输出:false
- 输入:[], k=0 → 输出:false
- 输入:[1,2,3,1,2,3], k=2 → 输出:false
-
性能用例:
- 大数组(10^5元素)中有重复但距离远
- 大数组中每隔k个元素就有重复
测试函数示例:
c复制void test() {
int nums1[] = {1,2,3,1};
assert(containsNearbyDuplicate(nums1, 4, 3) == true);
int nums2[] = {1,2,3,1,2,3};
assert(containsNearbyDuplicate(nums2, 6, 2) == false);
int nums3[] = {1};
assert(containsNearbyDuplicate(nums3, 1, 0) == false);
printf("All tests passed!\n");
}
6. 常见问题与调试技巧
6.1 哈希表实现中的常见错误
-
哈希函数设计不当导致大量冲突
- 解决方法:使用更好的哈希函数,如乘法哈希
-
未正确处理负数键
- 解决方法:在哈希函数中对负数做适当处理
-
哈希表大小不足
- 解决方法:根据问题规模调整HASH_SIZE
6.2 调试技巧
-
打印哈希表内容:
c复制void printHashTable() { for (int i = 0; i < HASH_SIZE; i++) { if (hashTable[i].key != 0) { printf("Index %d: key=%d, value=%d\n", i, hashTable[i].key, hashTable[i].value); } } } -
添加详细的日志输出:
c复制printf("Processing index %d, num=%d, lastPos=%d\n", i, nums[i], lastPos); -
使用Valgrind检查内存错误
6.3 性能优化建议
- 对于小规模k值,滑动窗口法可能更高效
- 可以尝试动态调整哈希表大小
- 对于已知范围的输入数据,可以使用更简单的哈希函数
7. 扩展思考
7.1 类似题目推荐
- LeetCode 217 - 存在重复元素(更简单版本)
- LeetCode 220 - 存在重复元素III(更难版本,需要考虑值差和下标差)
- LeetCode 76 - 最小覆盖子串(滑动窗口高级应用)
7.2 实际应用场景
这种算法在以下场景有实际应用:
- 检测文档中相近的重复内容
- 网络数据包去重
- 基因组序列分析
7.3 进一步学习建议
- 深入学习哈希表的各种实现方式
- 研究更多字符串匹配算法
- 了解布隆过滤器等概率数据结构
在实际编码面试中,这类题目考察的重点不仅是写出正确的代码,还包括:
- 对时间/空间复杂度的分析能力
- 边界条件的考虑全面性
- 代码的可读性和模块化程度
因此,建议在练习时不仅要通过测试用例,还要思考如何写出工业级的代码质量。
