不修改数组找出重复的数字
1、题目
在一个长度为 n+1 的数组里的所有数字都在 1~n 的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为 8 的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字 2 或 3。
输入参数:一个整数数组numbers,数组长度length
输出参数:-1(代表输入参数出错),或者是重复的数字
2、解题
这道题的关键在于二分法的使用。
该题有两种解法:
- 使用O(n)的辅助空间,将原数组复制到新的数组中,然后看有没有重复的数字,和哈希表方法类似
- 使用二分法,不断缩小存在重复数字的数组范围,直到找到为止,空间复杂度为O(1)
下面是方法二的具体步骤:
- 把从1~n的数字从中间的数字m分为两部分,前面一半为1~m,后面一半为m+1~n
- 如果1~m的数字的数目超过m,那么这一半的区间里一定包含重复的数字;否则,另一半m+1~n的区间里一定包含重复的数字。
- 继续把包含重复数字的区间一分为二,直到找到一个重复的数字。
方法二的特点
-
效率
按二分查找的思路,函数countRange将被调用O(logn)次,每次需要O(n)的时间,总时间复杂度为O(nlogn)
与前面提到的需要O(n)空间的方法一相比,这种算法相当于以时间换空间
-
缺点
这种算法有个挺大的缺点,就是不能保证找出重复的数字。
比如,不能找出数组{2, 3, 5, 4, 3, 2, 6, 7}中重复的数字2,因为在1~2范围内有1、2两个数字,而这个范围的数字也只出现了两次(两个2),此时这种算法无法判断是每个数字各出现一次还是某个数字出现了两次。
-
总结
在面试时,一定要先弄清楚面试官的需求,根据他提出的不同功能要求(找出任意一个重复数字、找出所有重复数字),或者性能要求(时间效率优先、空间效率优先)来选取最终算法。
3、代码
int countRange(const int* numbers, int length, int start, int end) {
//鲁棒性检查
if (numbers == nullptr)
return 0;
int count = 0;
//统计数组指定位置中大于等于start小于等于end的个数
for (int i = 0; i < length; i++) {
if (numbers[i] >= start && numbers[i] <= end)
count++;
}
return count;
}
int getDuplication(const int* numbers, int length) {
//鲁棒性检查
if (numbers == nullptr || length <= 0)
return -1;
for (int i = 0; i < length; i++)
if (numbers[i] < 1 || numbers[i] > length - 1)
return -1;
//初始化
int start = 1;
int end = length - 1;
//开始循环查找
while (start <= end) {
//先获得中间元素,再统计指定数组范围内的元素个数
int middle = ((end - start) >> 1) + start;
int count = countRange(numbers, length, start, middle);
//如果只剩最后一个元素
if (end == start) {
//当找到就返回元素
if (count > 1)
return start;
//没找到就返回-1
else
break;
}
//如果未剩下最后一个元素,进一步确定范围
if (count > (middle - start + 1))
end = middle;
else
start = middle + 1;
}
return -1;
}
4、注意点
- 统计数组指定位置中大于等于start小于等于end的个数时,记得先进行数组的鲁棒性检查
- 计算中间元素middle时,使用右移运算符 >>,并且加上的是start而不是1
- 进一步确定范围时,如果范围在数组后方,则start应赋值为middle+1而不是middle
- 需要在while循环的起始处通过start是否等于end,判断范围是否已经缩小到只剩下最后一个元素