不修改数组找出重复的数字

学习笔记
题目

 一个长度为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
欢迎提出不同意见

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值