学习笔记
题目
一个长度为n + 1的数组里面的所有数字都在1 ~ n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{7,7,5,4,2,6,1,3},那么对应输出的应该是7。
题目分析
这道题有个明显的特征——数据的值在一个固定的范围内。题目给出的要求是数字范围为1~n的数组有n + 1个元素,所以在输入合法的时候,一定存在重复元素。解决这一类问题可以使用hash表,又或者使用长度为n的辅助数组,将元素的值作为辅助数组的下标,只要遍历一遍数组就可以获得答案,其时间复杂度都是O(n),但是需要O(n)的空间,所以这个是空间换时间的做法。
hash表或者使用辅助数组都可以解决问题,但是有时也会遇到空间效率优先的情况。
剑指Offer中提出了一种以时间换空间的算法。
算法描述
需要注意的是,这个算法划分区间是和元素的取值范围有关的,书中使用的是1 ~ n,如果是其他范围,可能会不适用,或者需要修改相关变量。
其次是这个算法类似于二分查找或者是折半查找,都是先将查找对象分为两部分,每次对其中一部分进行查询,但是不要求元素已经排好序。
前面说到这道题目比较明显的特征是数组元素的值的范围是限定为1 ~ n的,而元素个数则有n + 1个,那么很明显至少存在一个元素的值是重复的,例如n = 5。
例1.当n = 5,且数组为{1,2,3,4,5,3}时,假设不存在重复数字,数组元素取值范围为1~ 5时最多只有5个数,而这里有6个数,所以必然存在重复数字。
所以我们可以尝试每次只检测一定范围内的数值,判断这个范围内是否存在重复数字,如果这个范围内的元素的出现次数(每次都需要遍历整个数组)比该范围应有的元素个数多的时候,那么这个范围就有重复的数字,否则重复数字就在另一个范围内。
例2.设搜索对象为{1,2,3,4,5,3}。我们的搜索过程将由以下几个步骤组成:
1.将1 ~ n的数字从中间的数字m分为两部分,在这里m = 3,即{1,2,3}和{4,5,3}
2.先计算{1,2,3}中元素在数组中的出现次数之和,即对于1,2,3都遍历一遍整个数组,得到结果为4,显然出现次数比1~3范围内应有的元素个数多,所以重复数字在这个范围中。
3.将{1,2,3}再按中间数字分为两部分,即{1,2}和{3},经计算可得{1,2}中元素在数组中出现次数为2,等于该范围内的数字个数,所以我们转而判断另一部分,也就是{3},因为只有一个元素,所以得到结果。
这个算法和二分查找类似,计算某一范围的数字在数组中出现频数的次数大概为O(logn)次,而每次都需要完整遍历一遍整个数组,也就是O(n)的时间,所以总的来说时间复杂度为O(nlogn),而空间复杂度为O(1)。
相比于hash表还有使用辅助数组,这种方法并不能找出所有重复的数字,而是只能找出其中一个。所以还是需要根据需求决定是否选择这种算法。
C++ 代码
/*先将元素划分为两部分,计算前面部分的元素在整个数组中出现的次数,如果出现次数大于元素个数,
*则重复数字在这一部分,否则在另一部分,重复以上步骤即可*/
#include <iostream>
using namespace std;
#define nullptr NULL
int countRange(const int *numbers,int length,int start,int end){
if(numbers == nullptr)
return 0;
int count = 0;
for(int i = 0;i < length;i++){
//计算在[start,end]之间的元素在整个数组内的出现次数
if(numbers[i] >= start && numbers[i] <= end)
++count;
}
return count;
}
int GetDuplication(const int *numbers,int length){
if(numbers == nullptr || length <= 0)
return -1;
int start = 1; //题目限定范围为1 ~ n,所以划分的时候起始位置为1
int end = length - 1;
//所以start和end在这里并不是作为数组下标,而是作为划分的起始和结束位置
while(end >= start){
int middle = ((end - start) >> 1) + start;
int count = countRange(numbers,length,start,middle); //获得元素出现次数之和
if(end == start){
if(count > 1)
return start;
else
break;
}
//当前划分含有重复数字时,下一个循环就是对该划分进行二次划分
if(count > (middle- start + 1))
end = middle;
else start = middle + 1; //否则就对另一个划分进行二次划分
}
return -1;
}
int main(){
int array[8] = {7,7,5,4,2,6,1,3};
cout<<GetDuplication(array,8);
return 0;
}
本博文参考自剑指Offer
欢迎提出不同意见