1. 题目解析与解题思路
LeetCode 219题"存在重复元素II"是一道经典的数组处理题目,题目要求判断数组中是否存在两个相同的元素,且它们的下标差不超过给定的k值。这道题在面试中经常出现,因为它很好地考察了候选人对基础数据结构的掌握程度和算法优化能力。
题目描述:给定一个整数数组nums和一个整数k,判断数组中是否存在两个不同的索引i和j,使得nums[i] == nums[j],并且i和j的差的绝对值不超过k。
1.1 暴力解法分析
最直观的解法是使用双重循环遍历数组,对每个元素检查其后k个元素中是否有相同的值。这种方法的时间复杂度是O(nk),在k较大时效率会很低。
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;
}
虽然这种解法简单直接,但对于大规模数据来说效率太低,特别是当k接近n时,时间复杂度会退化为O(n²)。
1.2 优化思路
为了提高效率,我们需要寻找更优的数据结构来存储和查询元素。哈希表(Hash Table)是一个理想的选择,因为它可以在平均O(1)时间内完成插入和查询操作。
基本思路是:
- 遍历数组,维护一个哈希表记录元素及其最近出现的位置
- 对于当前元素,检查是否已经在哈希表中存在
- 如果存在且下标差≤k,返回true
- 否则更新哈希表中该元素的位置为当前位置
- 如果遍历结束都没有找到符合条件的元素对,返回false
这种解法的时间复杂度可以降低到O(n),空间复杂度也是O(n),因为最坏情况下需要存储所有元素。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C语言实现详解
2.1 哈希表的选择与实现
在C语言中,标准库没有提供现成的哈希表实现,我们需要自己实现一个简单的哈希表。考虑到题目中的元素是整数,我们可以使用开放寻址法来实现哈希表。
c复制#define HASH_SIZE 10000
typedef struct {
int key;
int value;
} HashNode;
HashNode hashTable[HASH_SIZE];
void initHashTable() {
for (int i = 0; i < HASH_SIZE; i++) {
hashTable[i].key = INT_MIN;
}
}
int hash(int key) {
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
void insert(int key, int value) {
int index = hash(key);
while (hashTable[index].key != INT_MIN) {
index = (index + 1) % HASH_SIZE;
}
hashTable[index].key = key;
hashTable[index].value = value;
}
int search(int key) {
int index = hash(key);
while (hashTable[index].key != INT_MIN) {
if (hashTable[index].key == key) {
return hashTable[index].value;
}
index = (index + 1) % HASH_SIZE;
}
return -1;
}
void removeKey(int key) {
int index = hash(key);
while (hashTable[index].key != INT_MIN) {
if (hashTable[index].key == key) {
hashTable[index].key = INT_MIN;
return;
}
index = (index + 1) % HASH_SIZE;
}
}
2.2 完整解题代码
结合上述哈希表实现,我们可以写出完整的解题代码:
c复制#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define HASH_SIZE 10000
typedef struct {
int key;
int value;
} HashNode;
HashNode hashTable[HASH_SIZE];
void initHashTable() {
for (int i = 0; i < HASH_SIZE; i++) {
hashTable[i].key = INT_MIN;
}
}
int hash(int key) {
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
void insert(int key, int value) {
int index = hash(key);
while (hashTable[index].key != INT_MIN) {
index = (index + 1) % HASH_SIZE;
}
hashTable[index].key = key;
hashTable[index].value = value;
}
int search(int key) {
int index = hash(key);
while (hashTable[index].key != INT_MIN) {
if (hashTable[index].key == key) {
return hashTable[index].value;
}
index = (index + 1) % HASH_SIZE;
}
return -1;
}
void removeKey(int key) {
int index = hash(key);
while (hashTable[index].key != INT_MIN) {
if (hashTable[index].key == key) {
hashTable[index].key = INT_MIN;
return;
}
index = (index + 1) % HASH_SIZE;
}
}
bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
initHashTable();
for (int i = 0; i < numsSize; i++) {
int prevIndex = search(nums[i]);
if (prevIndex != -1 && i - prevIndex <= k) {
return true;
}
insert(nums[i], i);
}
return false;
}
2.3 滑动窗口优化
上述实现虽然时间复杂度已经是O(n),但空间复杂度在最坏情况下也是O(n)。我们可以进一步优化空间复杂度,使用滑动窗口的思想将空间复杂度降低到O(k)。
滑动窗口的思路是维护一个大小为k的窗口,窗口内的元素存储在哈希表中。当窗口滑动时,移除离开窗口的元素,添加新进入窗口的元素。
c复制bool containsNearbyDuplicate(int* nums, int numsSize, int k) {
initHashTable();
for (int i = 0; i < numsSize; i++) {
if (i > k) {
removeKey(nums[i - k - 1]);
}
int prevIndex = search(nums[i]);
if (prevIndex != -1) {
return true;
}
insert(nums[i], i);
}
return false;
}
这种优化在k远小于n时能显著减少内存使用,同时保持O(n)的时间复杂度。
3. 边界条件与测试用例
3.1 常见边界情况
在实现算法时,需要考虑以下边界情况:
- 空数组或数组长度为1的情况
- k=0的情况
- 数组中存在多个重复元素但都不满足下标差条件
- 数组中存在多个重复元素且部分满足下标差条件
- 数组中所有元素都相同且k足够大
3.2 测试用例设计
针对上述边界情况,我们可以设计以下测试用例:
c复制void test() {
// 测试用例1:普通情况
int nums1[] = {1,2,3,1};
printf("Test 1: %d\n", containsNearbyDuplicate(nums1, 4, 3)); // 应输出1
// 测试用例2:k=0
int nums2[] = {1,2,3,1};
printf("Test 2: %d\n", containsNearbyDuplicate(nums2, 4, 0)); // 应输出0
// 测试用例3:空数组
int nums3[] = {};
printf("Test 3: %d\n", containsNearbyDuplicate(nums3, 0, 3)); // 应输出0
// 测试用例4:所有元素相同
int nums4[] = {1,1,1,1};
printf("Test 4: %d\n", containsNearbyDuplicate(nums4, 4, 1)); // 应输出1
// 测试用例5:无重复元素
int nums5[] = {1,2,3,4};
printf("Test 5: %d\n", containsNearbyDuplicate(nums5, 4, 3)); // 应输出0
// 测试用例6:多个重复但只有部分满足条件
int nums6[] = {1,2,3,1,2,3};
printf("Test 6: %d\n", containsNearbyDuplicate(nums6, 6, 2)); // 应输出0
}
4. 性能分析与优化
4.1 时间复杂度分析
哈希表解法的时间复杂度主要取决于哈希表操作的效率。在理想情况下,哈希表的插入、查找和删除操作都是O(1)时间复杂度,因此整个算法的时间复杂度是O(n)。
滑动窗口优化版本的时间复杂度仍然是O(n),因为每个元素最多被插入和删除一次。
4.2 空间复杂度分析
基本哈希表解法的空间复杂度是O(n),因为最坏情况下需要存储所有元素。
滑动窗口优化版本的空间复杂度是O(k),因为我们只维护窗口内的元素。
4.3 哈希冲突处理
在我们的实现中,使用开放寻址法处理哈希冲突。当冲突发生时,我们线性探测下一个可用的位置。这种方法简单但可能导致聚集现象。在实际应用中,可以考虑以下优化:
- 使用更好的哈希函数减少冲突
- 使用二次探测或双重哈希减少聚集
- 动态调整哈希表大小以保持较低的装载因子
c复制// 改进的哈希函数示例
int hash(int key) {
key = ((key >> 16) ^ key) * 0x45d9f3b;
key = ((key >> 16) ^ key) * 0x45d9f3b;
key = (key >> 16) ^ key;
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
4.4 内存管理优化
对于大规模数据,可以考虑动态分配哈希表内存而不是使用固定大小的数组。这样可以更灵活地处理不同规模的数据集。
c复制HashNode* hashTable;
int currentSize;
void initHashTable(int size) {
currentSize = size;
hashTable = (HashNode*)malloc(currentSize * sizeof(HashNode));
for (int i = 0; i < currentSize; i++) {
hashTable[i].key = INT_MIN;
}
}
void resizeHashTable(int newSize) {
HashNode* oldTable = hashTable;
int oldSize = currentSize;
initHashTable(newSize);
for (int i = 0; i < oldSize; i++) {
if (oldTable[i].key != INT_MIN) {
insert(oldTable[i].key, oldTable[i].value);
}
}
free(oldTable);
}
5. 实际应用与扩展
5.1 实际应用场景
这种算法在实际中有多种应用场景:
- 检测文档中相近的重复内容
- 网络数据包去重
- 基因组序列分析
- 缓存系统中检测最近访问的数据
5.2 算法扩展
基于这个题目,可以扩展解决更复杂的问题:
- 查找所有满足条件的元素对而不仅仅是判断是否存在
- 处理更复杂的相似性条件而不仅仅是相等
- 扩展到二维或多维数据
例如,查找所有重复元素对的扩展实现:
c复制void findAllNearbyDuplicates(int* nums, int numsSize, int k) {
initHashTable();
for (int i = 0; i < numsSize; i++) {
int prevIndex = search(nums[i]);
if (prevIndex != -1 && i - prevIndex <= k) {
printf("Found pair at indices %d and %d\n", prevIndex, i);
}
insert(nums[i], i);
}
}
5.3 多语言实现对比
虽然本文重点讲解C语言实现,但了解其他语言的实现方式也有助于加深理解:
Python实现(利用字典):
python复制def containsNearbyDuplicate(nums, k):
num_dict = {}
for i, num in enumerate(nums):
if num in num_dict and i - num_dict[num] <= k:
return True
num_dict[num] = i
return False
Java实现(利用HashMap):
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;
}
对比不同语言的实现可以看出,虽然语法不同,但核心算法思想是一致的。C语言实现需要更多底层细节处理,而高级语言则可以利用内置的数据结构简化代码。
6. 常见问题与调试技巧
6.1 常见问题
-
哈希表大小不足:如果HASH_SIZE设置过小,会导致频繁冲突,影响性能。应根据数据规模合理设置大小。
-
哈希函数设计不当:差的哈希函数会导致大量冲突。应选择分布均匀的哈希函数。
-
未初始化哈希表:忘记初始化哈希表会导致搜索错误。务必在使用前初始化。
-
边界条件处理不当:特别是当k≥n时,需要正确处理。
-
内存泄漏:动态分配内存的版本需要确保正确释放内存。
6.2 调试技巧
- 打印中间结果:在哈希表操作前后打印关键变量,帮助定位问题。
c复制void debugPrintHashTable() {
printf("Hash Table Contents:\n");
for (int i = 0; i < HASH_SIZE; i++) {
if (hashTable[i].key != INT_MIN) {
printf("[%d]: key=%d, value=%d\n", i, hashTable[i].key, hashTable[i].value);
}
}
}
-
单元测试:为每个函数编写测试用例,确保各组件正常工作。
-
使用断言:在关键位置添加断言检查不变量。
c复制void insert(int key, int value) {
int index = hash(key);
int start = index;
do {
if (hashTable[index].key == INT_MIN) {
hashTable[index].key = key;
hashTable[index].value = value;
return;
}
index = (index + 1) % HASH_SIZE;
} while (index != start);
// 哈希表已满
assert(0 && "Hash table is full");
}
-
性能分析:对于大规模数据,使用性能分析工具检测瓶颈。
-
内存检查工具:如Valgrind,检测内存泄漏和非法访问。
6.3 性能优化实践
在实际编码中,可以通过以下方式进一步优化性能:
- 内联小函数:对于hash()等简单函数,可以使用inline关键字提示编译器内联展开。
c复制static inline int hash(int key) {
return (key % HASH_SIZE + HASH_SIZE) % HASH_SIZE;
}
-
缓存友好访问:优化数据结构布局,提高缓存命中率。
-
减少分支预测失败:重构条件判断,使常见路径更直接。
-
并行化处理:对于超大数组,可以考虑分段并行处理。
-
特定数据优化:如果知道数据分布特征,可以针对性地优化哈希函数。
7. 学习资源与进阶路径
7.1 推荐学习资源
-
书籍:
- 《算法导论》 - 全面系统的算法理论基础
- 《C程序设计语言》 - C语言经典教材
- 《数据结构与算法分析》 - 实用的数据结构实现指南
-
在线课程:
- LeetCode官方教程
- Coursera上的算法专项课程
- MIT OpenCourseWare的算法课程
-
实践平台:
- LeetCode题库
- HackerRank算法挑战
- Codeforces编程竞赛
7.2 进阶学习路径
-
掌握更多数据结构:学习更复杂的数据结构如平衡二叉搜索树、跳表等。
-
深入算法设计:学习分治、动态规划、贪心等高级算法设计技巧。
-
系统性能优化:了解计算机体系结构,编写更高效的代码。
-
参与开源项目:通过实际项目提升工程能力。
-
参加编程竞赛:在时间压力下锻炼快速解题能力。
7.3 相关题目推荐
掌握了本题后,可以尝试以下相关题目巩固知识:
- LeetCode 217 - 存在重复元素(更简单的版本)
- LeetCode 220 - 存在重复元素III(更难的版本)
- LeetCode 3 - 无重复字符的最长子串(滑动窗口经典应用)
- LeetCode 76 - 最小覆盖子串(滑动窗口进阶)
- LeetCode 438 - 找到字符串中所有字母异位词(滑动窗口变种)
对于C语言学习者来说,实现这些题目不仅能提升算法能力,还能加深对C语言特性的理解,如指针操作、内存管理等。
