1. 问题描述与理解
LeetCode 961题要求我们找出在大小为2N的数组中重复N次的元素。题目描述很简单:给定一个长度为2N的数组nums,其中恰好有一个元素重复了N次,其余每个元素都只出现一次。我们需要找出这个重复N次的元素。
这个问题的关键在于理解题目给出的特殊条件。数组长度为2N,其中N+1个不同的元素(因为有N-1个元素各出现一次,1个元素出现N次)。这种特殊的分布给了我们优化解法的空间。
注意:题目保证输入数组一定满足条件,不需要处理非法输入的情况。这在实际面试中很重要,可以节省边界条件检查的时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法分析
2.1 暴力解法
最直观的解法是使用双重循环遍历数组,统计每个元素的出现次数:
c复制int repeatedNTimes(int* nums, int numsSize) {
for (int i = 0; i < numsSize; i++) {
int count = 0;
for (int j = 0; j < numsSize; j++) {
if (nums[j] == nums[i]) count++;
}
if (count == numsSize / 2) return nums[i];
}
return -1; // 题目保证不会执行到这里
}
时间复杂度:O(n²)
空间复杂度:O(1)
虽然这个解法能通过测试,但在LeetCode上会超时,显然不是最优解。
2.2 哈希表解法
使用哈希表可以优化时间复杂度:
c复制#include <stdlib.h>
typedef struct {
int key;
int count;
} HashEntry;
int repeatedNTimes(int* nums, int numsSize) {
HashEntry* hash = calloc(numsSize, sizeof(HashEntry));
for (int i = 0; i < numsSize; i++) {
// 查找是否已存在
for (int j = 0; j < numsSize; j++) {
if (hash[j].key == nums[i]) {
hash[j].count++;
if (hash[j].count == numsSize / 2) {
int res = hash[j].key;
free(hash);
return res;
}
break;
}
if (hash[j].key == 0) { // 空槽
hash[j].key = nums[i];
hash[j].count = 1;
break;
}
}
}
free(hash);
return -1;
}
时间复杂度:平均O(n),最坏O(n²)(哈希冲突时)
空间复杂度:O(n)
这个解法在C语言中实现起来比较麻烦,需要手动管理哈希表。在实际面试中,如果语言支持内置哈希表会更方便。
3. 优化解法探索
3.1 利用题目特性
观察题目特性:数组中有N+1个不同元素,其中1个出现N次,其他各出现1次。这意味着重复元素必须至少有两个相邻或间隔一个位置。
基于这个观察,我们可以设计O(n)时间、O(1)空间的算法:
c复制int repeatedNTimes(int* nums, int numsSize) {
// 检查相邻的三个位置
for (int i = 0; i < numsSize - 1; i++) {
if (nums[i] == nums[i + 1]) return nums[i];
if (i < numsSize - 2 && nums[i] == nums[i + 2]) return nums[i];
}
// 如果前面没找到,最后一个元素必然是重复元素
return nums[numsSize - 1];
}
时间复杂度:O(n)
空间复杂度:O(1)
这个解法利用了重复元素必须出现在特定位置的性质,非常高效。
3.2 随机采样法
另一种有趣的解法是随机采样:
c复制#include <stdlib.h>
#include <time.h>
int repeatedNTimes(int* nums, int numsSize) {
srand(time(0));
while (1) {
int i = rand() % numsSize;
int j = rand() % numsSize;
if (i != j && nums[i] == nums[j]) {
return nums[i];
}
}
}
时间复杂度:期望O(1),最坏O(∞)(理论上可能永远找不到)
空间复杂度:O(1)
虽然这个解法理论上可能很慢,但实际上由于重复元素占了数组的一半,平均几次尝试就能找到。
4. C语言实现细节
4.1 内存管理
在C语言中实现哈希表解法时,需要注意内存管理:
c复制// 创建哈希表
HashEntry* hash = calloc(numsSize, sizeof(HashEntry));
if (!hash) {
// 处理内存分配失败
return -1;
}
// ...使用哈希表...
// 使用完毕后释放内存
free(hash);
4.2 边界条件处理
虽然题目保证输入合法,但良好的编程习惯应该包含基本检查:
c复制if (!nums || numsSize < 2 || numsSize % 2 != 0) {
return -1; // 或处理错误
}
4.3 性能优化
对于优化解法,可以进一步减少比较次数:
c复制int repeatedNTimes(int* nums, int numsSize) {
for (int k = 1; k <= 3; k++) {
for (int i = 0; i < numsSize - k; i++) {
if (nums[i] == nums[i + k]) {
return nums[i];
}
}
}
return nums[0]; // 所有情况都满足时才会执行到这里
}
5. 测试用例设计
验证算法正确性的测试用例:
-
基本用例:
- 输入:[1,2,3,3]
- 输出:3
-
重复元素在开头:
- 输入:[2,2,1,3]
- 输出:2
-
重复元素在结尾:
- 输入:[1,3,2,2]
- 输出:2
-
重复元素间隔出现:
- 输入:[5,1,5,2,5,3,5,4]
- 输出:5
-
大数组测试:
- 输入:[1,2,...,999,1000,1000,...,1000] (共1000个1000)
- 输出:1000
6. 常见问题与解决
6.1 为什么优化解法只需要检查相邻和隔一个位置?
因为在一个2N的数组中,如果有一个元素出现N次,那么根据鸽巢原理,至少有两个该元素之间的距离不超过2。这是数学上的必然结果。
6.2 如何处理非常大的数组?
对于特别大的数组,哈希表解法可能消耗过多内存。此时优化解法(检查相邻位置)是最佳选择,因为它只需要O(1)的额外空间。
6.3 如果题目不保证输入合法怎么办?
需要添加输入验证:
c复制if (numsSize % 2 != 0) return -1;
// 检查是否确实有一个元素出现N次
7. 算法复杂度对比
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力 | O(n²) | O(1) | 不推荐 |
| 哈希表 | O(n) | O(n) | 通用解法 |
| 优化检查 | O(n) | O(1) | 推荐 |
| 随机采样 | O(1)期望 | O(1) | 有趣但不稳定 |
8. 实际应用场景
虽然这个问题看起来是纯算法题,但它实际上训练了我们几个重要的编程能力:
- 观察数据特性:发现重复元素必须出现在特定位置的性质
- 空间优化:如何在O(1)空间内解决问题
- 概率思维:随机采样法的设计
这些能力在实际开发中非常有用,比如:
- 处理大型数据集时的内存优化
- 设计高效的数据验证算法
- 实现快速近似算法
9. 扩展思考
这个问题可以扩展为更一般的形式:在一个大小为m×n的数组中,找出出现次数超过m次的元素。这种情况下,类似的优化思路仍然适用,但检查的范围需要相应调整。
另一个变种是找出所有出现超过一定次数的元素,这需要更复杂的算法,如Boyer-Moore投票算法的扩展版本。
10. C语言实现完整代码
以下是经过优化的完整C语言实现:
c复制#include <stdio.h>
int repeatedNTimes(int* nums, int numsSize) {
// 检查相邻和隔一个位置
for (int k = 1; k <= 3; ++k) {
for (int i = 0; i < numsSize - k; ++i) {
if (nums[i] == nums[i + k]) {
return nums[i];
}
}
}
// 理论上不会执行到这里
return nums[0];
}
// 测试代码
int main() {
int test1[] = {1,2,3,3};
printf("%d\n", repeatedNTimes(test1, 4)); // 3
int test2[] = {2,1,2,5,3,2};
printf("%d\n", repeatedNTimes(test2, 6)); // 2
int test3[] = {5,1,5,2,5,3,5,4};
printf("%d\n", repeatedNTimes(test3, 8)); // 5
return 0;
}
这个实现简洁高效,通过了LeetCode的所有测试用例。在实际编程中,这种利用问题特性的优化往往能带来显著的性能提升。
