找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
限制:
2 <= n <= 100000
方法一:哈希表 / Set
利用数据结构特点,容易想到使用哈希表(Set)记录数组的各个数字,当查找到重复数字则直接返回。算法流程:
- 初始化: 新建 HashSet ,记为 dic ;
- 遍历数组 nums 中的每个数字 num :
- 当 num 在 dic 中,说明重复,直接返回 num;
- 否则将 num添加至 dic 中;
- 返回 -1 。题中一定有重复数字,因此这里返回多少都可以。
复杂度分析:
- 时间复杂度 O(N) : 遍历数组使用 O(N),HashSet 添加与查找元素皆为 O(1) 。
- 空间复杂度 O(N) : HashSet 占用 O(N)大小的额外空间。
class Solution {
public int findRepeatNumber(int[] nums) {
// 使用set存放不重复元素
Set<Integer> dic = new HashSet<>();
// 遍历数组进行比较,如果set中不包含该元素,放入
for(int num : nums) {
if(dic.contains(num)) return num;
dic.add(num);
}
return -1;
}
}
在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此题意表示:数组元素的 索引 和 值 是 一对多 的关系。—— 遍历数组,通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] = i)。因而,就能通过索引映射对应的值,起到与字典等价的作用。
数组的数字都在 0~n-1的范围内。如果这个数组中没有重复的数字,当数组排序之后数字 i将出现在下标为 i 的位置。由于数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。
方法二:原地交换
遍历数组时,第一次遇到数字 x 时,将其交换至索引 x 处;而当第二次遇到数字 x 时,一定有 nums[x] = x ,此时即可得到一组重复数字。
以2,3,1,0,2,5,3为例
1)数组第0号位置为2,2!=0,将0号位置的2与2号位置的1交换,变成 1,3,2,0,2,5,3
2)数组第0号位置为1,1!=0,将0号位置的1与1号位置的3交换,变成 3,1,2,0,2,5,3
3)数组第0号位置为3,3!=0,将0号位置的3与3号位置的0交换,变成 0,1,2,3,2,5,3
4)数组第0号位置为0,0==0,游标右移至1号位置,1==1,继续右移,直到第4号位置
5)数组第4号位置为2, 2!=4, 2==array[2],说明2重复了,输出2,算法结束算法流程:
- 遍历数组 nums,设索引初始值为 i = 0:
- 若 nums[i] = i: 说明此数字已在对应索引位置,无需交换,因此跳过;
- 若 nums[nums[i]] = nums[i]: 代表索引 nums[i] 处和索引 i 处的元素值都为 nums[i] ,即找到一组重复值,返回此值 nums[i];
- 否则: 交换索引为 i 和 nums[i] 的元素值,将此数字交换至对应索引位置。
- 若遍历完毕尚未返回,则返回 -1 。
复杂度分析:
- 时间复杂度 O(N) : 遍历数组使用 O(N),每轮遍历的判断和交换操作使用 O(1) 。
- 空间复杂度 O(1) : 使用常数复杂度的额外空间。
class Solution {
public int findRepeatNumber(int[] nums) {
//初始化
/*HashMap map = new HashMap<Integer,Integer>();
for(int i = 0; i < nums.length; i++){
if(map.get(nums[i]) == null){
map.put(nums[i], 1);
}else{
return nums[i];
}
}*/
//置换
int length = nums.length;
int i = 0;
while(i < length){
if(nums[i] == i){
i++;
continue;
}
if(nums[nums[i]] == nums[i]){
return nums[i];
}else{
/*记num[ i ] =a, num[num[ i ]]=num[ a ] = b,
那么 交换后,num[ i ]=b, num[num[i]]=num[a]=a,
这时候下标 a 对应的元素也是a,达到目的
*/
int temp;
temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
}
return -1;
}
}
补充题:
一个长度大小为n的数组,数组中的每个元素的取值范围在[1,n],且为正整数。问:如何在时间复杂度为O(n),空间复杂度为O(1)的条件下,统计数组中不同元素出现的次数。
- 数组按序扫描,通过当前元素的值作为下标,找到下一个元素。
- 最后得到的数组中,下标(因为下标从0开始的,故输出时需要+1)为数组中出现的元素,每个下标对应的值取反输出即是该元素出现的频率。
- 若当前元素小于0,则跳过;
- 若当前元素大于0,则判断其作为下标索引到的元素是否大于0,
- 若大于0,则索引到的元素赋值给当前元素,索引到的元素置为-1;
- 若小于0,则索引到的元素自减1,当前元素置为0;
- 用正数/负数来区分a[i]上是原来的值,还是用于计数的值
举个例子,比如 { 2, 5, 5, 2, 3 }。看到第一个是 2,即2有1个,所以先将这个“1”保存到a[2-1]的位置上。但a[2-1]有有效数,将该数移动。假设不用负数,而是用中括号来表示计数,步骤依次是
{ 2, 5, 5, 2, 3 }
5, [1], 5, 2, 3
3, [1], 5, 2, [1]
5, [1], [1], 2, [1]
[0], [1], [1], 2, [2]
[0], [2], [1], [0], [2]结果是 1有0个, 2有2个, 3有1个, 4有0个, 5有2个
此类以值做下标的方法适用条件:正整数、取值范围[1,n]、n个数
package DaliyPractise;
import java.util.*;
/*一个长度大小为n的数组,数组中的每个元素的取值范围在[1,n],且为正整数。
问:如何在时间复杂度为O(n),空间复杂度为O(1)的条件下,统计数组中不同元素出现的次数。
*/
public class UnOrderedArrayNumberCount {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 数组的长度
int[] arr = new int[n]; // 数组元素的取值范围在【1,n】,正整数
for(int i = 0; i < n; i ++) {
arr[i] = sc.nextInt();
}
calNumberAppear(n, arr);
}
private static void calNumberAppear(int n, int[] arr) {
// TODO Auto-generated method stub
if(arr == null || n < 0 || arr.length == 0) {
return;
}
int index = 0; // 从第一位开始遍历
while(index < n) {
// 计算当下元素的在数组下标中的位置 [0 , n - 1]
int temp = arr[index] - 1;
// 如果出现 负数,相当于数字已经被替换变为计数表示,继续向后执行
if(temp < 0) {
index ++;
continue;
}
// 让该位置的元素放置到 与当前元素位置 值相同的数组位置上
// 将该位置的数值变为 -1,代表 当前元素位置相同的数出现了一次
if(arr[temp] > 0) {
arr[index] = arr[temp];
arr[temp] = -1;
}
else {
// 如果当下下标的元素 <= 0,相当于已经出现过,次数++即 --
arr[temp] --;
arr[index] = 0;
}
}
// 输出结果,将负数取反即为最后的结果
for(int i = 0; i < n; i ++) {
System.out.print(Math.abs(arr[i]) + " ");
}
}
}
/*
5
2 5 5 2 3
0 2 1 0 2
*/