题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
例如,如果输入长度为7的数组numbers {2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2
这道题我将提供三种解法:1.使用java提供的集合类的解法 2.使用长度为n的boolean数组记录重复情况 3.扫描并将元素移动到数组对应位置排查重复情况
解法一:
我们知道,数学中“集合”的特征之一就是:集合中不能存在重复的元素。因此我们可以将数组中的元素依次存入集合中并判断集合中是否已经有了这个数字,直到重复数字存入时便会被发现。在代码中,我们使用java合集框架中的HashSet进行实现:
import java.util.HashSet;
import java.lang.Integer;
public class Solution {
//方法一,使用了HashSet
public boolean duplicate1(int numbers[],int length,int [] duplication)
{
if(numbers==null) //注意numbers指向null的情况
{
return false;
}
HashSet<Integer> hashSet=new HashSet<>(); //集合记录已经遍历过的数字
for(int i=0;i<numbers.length;i++)
{
if(hashSet.contains(numbers[i])) //判断集合中是否已经存在该数字
{
duplication[0]=numbers[i];
return true;
}
else
{
hashSet.add(numbers[i]);
}
}
return false;
}
}
时间复杂度:需要对数组numbers进行一次线性遍历,因此是线性时间复杂度O(n)
空间复杂度:需要使用HashSet存储数组numbers中的每个元素,因此是O(n)
调用HashSet可以帮助我们简便优雅的完成集合相关的一系列操作,不需要自己定义集合数据结构并实现相关集合操作方法,但是否有不需要调用java类库的解法,同时保证代码结构的简单?
解法二:
定义一个和数组numbers长度相等(即长度为n)的boolean类型数组exit,所有元素值初始化为false。当遍历到N=numbers[i]时,若exit[N]为false,将它置为true,表示已经遍历过;若exit[N]为true,则找到了重复数字:
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(numbers==null)
{
return false;
}
boolean[] exit=new boolean[numbers.length]; //当遍历到数字i,则boolean[i]置true
for(int i=0;i<numbers.length;i++)
{
if(exit[numbers[i]]==false)
{
exit[numbers[i]]=true;
}
else
{
duplication[0]=numbers[i];
return true;
}
}
return false;
}
}
时间复杂度:需要对数组numbers进行一次线性遍历,因此是线性时间复杂度O(n)
空间复杂度:需要使用HashSet存储数组numbers中的每个元素,因此是O(n)
相比调用HashSet,一个boolean变量的大小只有1字节,虽然空间复杂度依然为O(n),但会比解法一少占用一些空间;且HashSet判断元素重复的方法是计算并比较元素值的HashCode,而哈希算法往往是依托数组进行底层实现,boolean数组使用下标可以直接读取指定位置元素,因此相比调用HashSet这种相对“重量级”的操作更加“轻量”
然而,我们能否在时空间杂度上进行进一步优化?
解法三:
前两种解法中,我们都需要一个长度为n的辅助空间。受解法二中将数字N与辅助数组boolean[N]对应起来的启发,我们可以遍历数组numbers,将numbers[i]放在数组numbers中下标为numbers[i]的位置——它的对应位置
当遍历到numbers[i]=N时:
- 若N==i,表示数字N在对应位置,则遍历下一个数字
- 若N!=i,则将N和number[N]做比较:
- 若numbers[N]和N相等,则找到了重复数字,否则:
- 将numbers[N]和N,也就是numbers[i]交换,使N被放到他的对应位置
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(numbers==null)
{
return false;
}
int i=0;
while(i<numbers.length)
{
if(numbers[i]==i)
{
i++;
}
else if(numbers[i]==numbers[numbers[i]]) //发现重复
{
duplication[0]=numbers[i];
return true;
}
else //交换位置,使得numbers[i]放入对应位置
{
int temp=numbers[i];
numbers[i]=numbers[numbers[i]];
numbers[temp]=temp;
}
}
return false;
}
}
时间复杂度:需要对数组numbers进行一次线性遍历,因此是线性时间复杂度O(n)
空间复杂度:只需要在交换操作中使用大小为1的辅助空间,因此是O(1)
因为数组是物理上占用连续空间的数据结构,可以利用下标轻松访问任何位置的元素,我们应该利用好数组的这个特性。同时值得注意的是:两个数组元素之间的交换是经常被使用的操作,我们在解法三中就使用了它,现实中也有诸如冒泡排序、简单选择排序等许多算法使用它