题目
在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但是不能修改输入的数组。例如,如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3。
解决思路
这题的难点在于不能修改输入的数组,下面由简到难讲述几个解决方案
思路1
由于不能修改输入的数组,我们可以创建一个长度为n+1的辅助数组,然后逐一把原数组的每个数字复制到辅助数组。如果原数组中被复制的数字是m,则把它复制到辅助数组中下标为m的位置。如果下标为m的位置上已经有数字了,则说明该数字重复了。由于使用了辅助空间,故该方案的空间复杂度是 O(n) 。下面是该算法的Java程序:
package TempFile;
/**
* Created by 余沾.
*/
public class Test {
/**
* 利用辅助数组解决该问题
*/
public int getDuplication(int[] arr)
{
int[] newArr = new int[arr.length];
for(int i = 0;i < arr.length;i++)
{
if(arr[i] < 0 || arr[i] >= arr.length)
throw new IllegalArgumentException("输入参数不合法");
else
newArr[i] = -1;
}
for(int i = 0;i < arr.length;i++)
{
if(newArr[arr[i]] != arr[i])
{
newArr[arr[i]] = arr[i];
}else
return arr[i];
}
return -1;
}
public static void main(String[] args) {
Test test = new Test();
int[] arr = {2,3,5,4,3,2,6,7};
int value = test.getDuplication(arr);
}
}
思路2
由于思路1的空间复杂度是 O(n) ,因此我们需要想办法避免使用辅助空间。我们可以想:如果数组中有重复的数,那么n+1个0~n范围内的数中,一定有几个数的个数大于1。那么,我们可以利用这个思路解决该问题。
我们把从1~n的数字从中间的数字m分为两部分,前面一半为1~m,后面一半为m+1~n。如果1~m的数字的数目等于m,则不能直接判断这一半区间是否包含重复的数字,反之,如果大于m,那么这一半的区间一定包含重复的数字;如果小于m,另一半m+1~n的区间里一定包含重复的数字。接下来,我们可以继续把包含重复的数字的区间一分为二,直到找到一个重复的数字。
由于如果1~m的数字的数目等于m,则不能直接判断这一半区间是否包含重复的数字,我们可以逐步减少m,然后判断1~m之间是否有重复的数,即,我们可以令m=m-1,然后再计算1~m的数字的数目是否等于m,如果等于m,再令m=m-1,如果大于m,则说明1~m的区间有重复的数,如果小于m,则说明m+1~n有重复的数,不断重复此过程。
下面是该算法的Java程序:
package TempFile;
/**
* Created by 余沾.
*/
public class Test {
/**
* 避免使用辅助空间
*/
public int getDuplication(int[] arr)
{
for(int i = 0;i < arr.length;i++)
{
if(arr[i] < 0 || arr[i] >= arr.length)
throw new IllegalArgumentException("输入参数不合法");
}
int start = 0;
int end = arr.length-1;
int flag = 0;
int middle = 0;
while(end >= start)
{
if(flag == 0)
middle = (end + start)/2;
int count = countRange(arr,start,middle);
if(end == start)
{
if(count > 1)
return start;
else
break;
}
if(count > (middle-start+1))//说明(start,middle)这个区间有重复的数
{
end = middle;
flag = 0;
}else if(count == (middle-start+1))//不能判断(start,middle)这个区间有重复的数
{
middle = middle - 1;
if(middle < start)//说明(start,middle)这个区间没有重复的数
{
start = (start+end)/2 + 1;
flag = 0;
}else
flag = 1;
}else //说明(middle+1,end)这个区间有重复的数
{
start = middle + 1;
flag = 0;
}
}
return -1;
}
private int countRange(int[] arr, int start, int end)
{
int count = 0;
for(int i = 0;i < arr.length;i++)
{
if(arr[i] >= start && arr[i] <= end)
++count;
}
return count;
}
public static void main(String[] args) {
Test test = new Test();
int[] arr = {0,3,5,4,1,2,6,7};
int value = test.getDuplication(arr);
}
}
上述代码按照二分查找的思路,如果输入长度为n的数组,那么函数countRange最多将被调用 O(logn) 次,每次需要 O(n) 的时间,因此总的时间复杂度是 O(nlogn) 。但是如果区间无重复的数,则时间复杂度将变为 O(n2) 。