不修改数组,找出重复的数字
在一个长度为 n+1 的数组里的所有数字都在 1~ n 的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。数组中某些数字是重复的,但是不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如:如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出应该是重复的数字2或者3
第一种方法:类似于桶排序,
创建一个额外的辅助数组,将原数组中的数字 m,复制到辅助数组中下标为 m 的位置,如果已经赋值过了,则肯定是重复的
时间复杂度:遍历了一次原数组,所以为O(n)
空间复杂度:创建了额外的辅助数组,O(n)
第二种方法:二分查找。
以范围内的中点记为 m, 如果 1-m 之间的数字,在原数组中出现了超过m次,则这个范围内肯定有重复数字;如果在这个范围内没有重复的数字,这个范围内的数字出现的次数肯定是小于等于 m 的。
时间复杂度:Count函数被调用了 logn 次,每次都遍历数组,所以为O(n*logn)
空间复杂度:O(1)
int[] numbers = {2, 3, 5, 4, 3, 2, 6, 7};
int len = numbers.length;
int start = 1; // 用于存储查找数字范围的开头
int end = len; // 用于存储查找数字范围的结尾
int middle = (end - start)/2 + start; // 用于存储,查找数字范围的中间数字
int count = 0; // 确定范围内数字出现的次数
while (start <= end){ // 这个几乎是永久成立的,只有break/return能退出循环
middle = (end - start)/2 + start; // 更新middle
count = Count(numbers, len, start, middle); // 统计
if (end == start){ // 如果 end=start,middle也等于start,就相当于统计的是start这个数字出现的次数
if (count > 1){
System.out.println(start);
break;
}else{
break;
}
}
if (count > (middle - start + 1)){ // 如果数字出现的次数大于范围,说明这个范围内有重复的数字
end = middle; // 缩小范围,继续统计
}else{
start = middle + 1; // 如果数字出现的次数小于范围,则说明范围内没有重复的数字,则缩小范围。
}
}
/* 统计在原数组中的所有数据,在 start~middle 这个范围内的数字出现的次数 */
public static int Count(int[] numbers, int len, int start, int middle){
int count = 0;
for(int i=0;i<len;i++){
if(numbers[i] >= start && numbers[i] <= middle){
count++;
}
}
return count;
}
测试用例:
- 长度为n的数组里包含1个或多个重复的数字
- 数组中不包含重复的数字。如果没有重复的数据,应该在循环结束之后返回
- 无效的输入测试用例
- 空数组:这里需要在主函数开头、统计函数开头进行判断,如果是空数组或者只有一个数字的数组,则直接返回没有
- 长度为n的数组中包含 大于 n-1 的数字:可以先遍历一次
问题:
我觉得这个算法有一个致命的缺陷,就是说,如果在范围内,比如1 ~ 4内, 只有3这个数字出现了3次,那么按照这个算法,虽然3也是重复的数字,但是它却找不到。也就是说,这个统计函数返回的数字无法确定是某个数字多次出现的结果,还是每个数字都出现了一次的结果。但是此时在 5-7这个范围内,必然还存在着其他重复的数字,这个算法能找到5-7范围内的重复的数字。