Q1:
给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次。
找到所有出现两次的元素。
例:
输入: [4,3,2,7,8,2,3,1]
输出: [2,3]
思考:
该类型题目的解法很多,如:
使用 hash 表类型的数据结构,扫描一遍数组,以数据值为 key,出现次数为 value,最后拿到所有 value 为2的 key。该思路的时间复杂度为 O( n ),空间复杂度为 O( n );
使用快速排序数组,在排序过程中,找到重复的数字。该思路的时间复杂度为 O( n*log(n) ),空间复杂度为 O( 1 )
以上两种方法均可以解出该题目,但不是最优解
A:
以下两种算法的时间复杂度为 O( n ), 空间复杂度为 O( 1 )
/**
* 这个题属于技巧题 首先仔细看输入的给定的数组值 该值的区间为 1 ≤ a[i] ≤ n
* 这其实是这道题解题的关键点,好好利用这个信息。 某些元素出现了两次,
* 而一些其他的元素只出现了1次,我们可以利用这些元素在出现次数上面的不一样做文章。
* <p>
* 仔细观察发现1 ≤ a[i] ≤ n 这个条件,正好和我们数组的下标差1,我们可以按照数值
* 来遍历数组,那么在数组中具有相同值的元素,会被经过两次,那么我们只要想出一种方式
* 在这个遍历结束后可以区分,哪些元素被经过了多次即可,由于数组元素具有1 ≤ a[i] ≤ n
* 这样的范围,那其实我们当每次经过一个元素时,给他加上n,当遍历结束时,我们再次遍历数组
* 那些数值超过2n的元素索引+1,对应的就是我们的出现了两次的元素。
*/
public static List<Integer> findDuplicates(int[] nums) {
int length = nums.length;
if (nums == null | length == 0) {
return null;
}
List<Integer> ret = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
int index = (nums[i] - 1) % length;//是为了保证得到一个小于length的数组索引
nums[index] += length;
}
for (int i = 0; i < length; i++) {
if (nums[i] > 2 * length) {//证明该位置的值出现过两次
ret.add(i + 1);//i+1代表的是该位置应该存在的值,而不是数组中该位置的实际值
}
}
return ret;
}
/**
* 在输入数组中用数字的正负来表示该位置所对应数字是否已经出现过。
* 遍历输入数组,给对应位置的数字取相反数,如果已经是负数,说明前面已经出现过,直接放入输出数组。
* @param num
* @return
*/
private static List<Integer> findDuplicates2(int[] num) {
List<Integer> duplicates = new ArrayList<>(num.length);
//使用负数来标记是否重复出现过
for (int i = 0; i < num.length; i++) {
int abs = Math.abs(num[i]);
if (num[abs - 1] > 0) {//题目中有条件说明1 ≤ a[i],说明没有0这种情况
num[abs - 1] *= -1;
}else {
duplicates.add(abs);
}
}
return duplicates;
}
Q2(Q1变种题目):
一个长度n的数组,所有数字大小[1,2n],其中有些元素出现两次而其他元素出现一次。用O( n )时间,O( 1 )空间找出它们
思考:
在考虑题目时,首先需要明白的一点:
在讨论算法事件复杂度时,是考虑最坏的情况和问题规模最大的时候.比如程序有O(N)和O(N2)的代码段,那问题规模很大的时候,也就是N很大的时候,N2是N的高阶无穷大,所以比N2小的时间复杂度都可以忽略了。当有多个 for 并列时,其时间复杂度还是 O( N ),不能认为代码中,一个 for 循环的时间复杂度为 O( N ),多个并列的 for 循环的时间复杂度就不是 O( N ),在一道考研算法题里,也是要求时间 O( N ),但是标准答案就是写了3个并列for循环。
明白上面的一点后,知道该题目中,可以使用多个并列的 for 循环来完成,也是满足题目要求的。实现的大体思路可以参考上面的 findDuplicates2 方法。
/**
* 可以将数组中的数转移到【1,n】之间,然后使用正负来标记是否出现过(类似于findDuplicates2方法的思想)。将数组分为两个大的方向处理:
* 1.小于n的:使用正负标记
* 2.复原,将所有的数变为正数
* 3.大于n的:a[a[n]%n]
*
* @param num3
* @return
*/
private static List<Integer> findDuplicatesPro(int[] num3) {
List<Integer> duplicates = new ArrayList<>();
int length = num3.length;
if (num3 == null || length == 0) {
return duplicates;
}
//1.在小于n的数中修改正负
for (int i = 0; i < length; i++) {
int numI = Math.abs(num3[i]) - 1;//代表数组的下标
if (numI >= length) {//等于length时,数组要越界
continue;
}
if (num3[numI] > 0) {
num3[numI] *= -1;
} else {
duplicates.add(numI+1);
}
}
//2.复原
for (int i = 0; i < length; i++) {
if (num3[i] < 0) {
num3[i] *= -1;
}
}
//3.处理大于n的数
for (int i = 0; i < length; i++) {
int numI = Math.abs(num3[i]) - 1;//代表数组的下标
if (numI < length) {
continue;
}
if (num3[numI % length] > 0) {
num3[numI % length] *= -1;
} else {
duplicates.add(numI + 1);
}
}
return duplicates;
}
最后,特别感谢同学金帮助我解决这个问题