LeetCode 算法题 - Set Mismatch(Java 实现)

这是悦乐书的第 279 次更新, 第 295 篇原创

01 看题和准备

今天介绍的是 LeetCode 算法题中 Easy 级别的第 147 题(顺位题号是 645). 集合 S 最初包含从 1 到 n 的数字. 但不幸的是, 由于数据错误, 集合中的一个数字被复制到集合中的另一个数字, 这导致重复一个数字而丢失另一个数字. 给定一个数组 nums, 表示错误后该集的数据状态. 要求先找到两次出现的数字, 然后找到丢失的数数字, 最后以数组的形式返回它们. 例如:

输入: nums = [1,2,2,4]

输出:[2,3]

注意:

给定的数组大小将在 [2,10000] 范围内.

给定数组的数字无序.

本次解题使用的开发工具是 eclipse,jdk 使用的版本是 1.8, 环境是 win7 64 位系统, 使用 Java 语言编写和测试.

02 第一种解法

题目的意思是根据给出的数组, 找出两个数, 第一个数是该数组中重复出现的那个数, 第二个数是该数组中缺失的那个数, 也就是说最后返回的结果数组, 两个数是有先后顺序的.

先对数组排序, 然后从第二位开始遍历数组, 如果当前一位与前一位相等, 说明重复了, 那么第一位数就找到了. 如果当前元素比前一位元素加 1 后还要大, 说明前一位元素是重复出现的那个数的第二个, 也就是要找的缺失的那个数.

但是, 还有两种特殊情况需要考虑, 因为题目并不能保证数组首尾不缺少元素. 第一, nums 的第一位是不是从 1 开始? 如果不是, 那么第二个数就是 1. 第二, nums 的最后一位元素是不是和数组长度相等? 如果不相等, 那么第二个数就是数组的长度.

此解法的时间复杂度是 O(nlog(n)), 空间复杂度是 O(1).publicint[]findErrorNums(int[]nums){

// 排序
Arrays.sort(nums);
int[]result=newint[2];
for(inti=1;i
if(nums[i]==nums[i-1]){
// 第一个数, 重复出现的元素
result[0]=nums[i];
}elseif(nums[i]>nums[i-1]+1){
// 第二个数, 缺失的那个数
result[1]=nums[i-1]+1;
}
}
// 数组第一个元素不是 1
if(nums[0]!=1){
result[1]=1;
}elseif(nums[nums.length-1]!=nums.length){
// 数组最后一个元素不是数组的长度
result[1]=nums.length;
}
returnresult;
}

03 第二种解法

我们也可以不排序, 使用 HashMap. 先将数组的元素都添加进 map 中, 以元素值为 key, 出现次数为 value. 然后开始循环, 从 0 到数组的长度, 如果 map 中包含当前索引值加 1, 并且它出现的次数为 2 次, 那么第一个数就是该元素, 否则第二个元素就是当前缺失的索引值加 1.

此解法的时间复杂度是 O(n), 最坏情况是 O(n^2), 空间复杂度是 O(n).publicint[]findErrorNums(int[]nums){

int[]result=newint[2];
Mapmap=newHashMap();
for(intnum:nums){
map.put(num,map.getOrDefault(num,0)+1);
}
for(inti=0;i
if(map.containsKey(i+1)){
// 出现两次的重复元素
if(map.get(i+1)==2){
result[0]=i+1;
}
}else{
// 缺失的元素
result[1]=i+1;
}
}
returnresult;
}

04 第三种解法

我们也可以不使用 HashMap, 使用数组进行代替. 思路与第二种解法类似.

此解法的时间复杂度是 O(n), 空间复杂度是 O(n).publicint[]findErrorNums(int[]nums){

int[]result=newint[2];
// 新建一个数组, 长度比 nums 大 1
int[]arr=newint[nums.length+1];
for(inti=0;i
// 以 nums 的元素作为索引, 出现次数为 value
arr[nums[i]]++;
}
// 数组的元素值是从 1 开始的, 所以新数组也从第二位开始遍历
for(inti=1;i
// 重复出现的元素
if(arr[i]==2){
result[0]=i;
}elseif(arr[i]==0){
// 缺失的元素
result[1]=i;
}
}
returnresult;
}

05 第四种解法

我们还可以使用减法. 先将数组元素正常情况下的元素之和算出来, 因为是从 1 到 n 的数, 是一个公差为 1 的等差数列, 可以直接套用等差数列求和的公式来计算和, 然后用算出来的和减去数组中的每一个元素, 最后和可能是正 1, 也可能是负 1, 因为数组中有重复元素, 也就可能多 1 或者少 1, 也即是重复项元素值多 1 或者少 1, 所以缺失的那个数可以直接使用最后的和与重复项元素相加得到.

此解法的时间复杂度是 O(n), 空间复杂度是 O(n).publicint[]findErrorNums(int[]nums){

int[]result=newint[2];
// 定义一个 HashSet
Setset=newHashSet();
// 等差数列求和公式
longsum=nums.length*(nums.length+1)/2;
for(inti=0;i
// 重复出现的那个数
if(set.contains(nums[i])){
result[0]=nums[i];
}
set.add(nums[i]);
// 减去每一个元素
sum-=nums[i];
}
// sum 最后可能是正数, 也可能是负数, 与重复元素相加后, 正好是缺失的那个数
result[1]=(int)sum+result[0];
returnresult;
}

06 第五种解法

我们可以对原数组进行标记, 将元素变成负数, 如果遇到的元素是小于 0 的, 说明已经对该元素变过一次负数, 也就是说该元素重复了, 最后在遍历一次数组, 如果遇到大于 0 的数, 表明在该位置缺少一个元素.

此解法的时间复杂度是 O(n), 空间复杂度是 O(1).publicint[]findErrorNums(int[]nums){

int[]result=newint[2];
for(intnum:nums){
if(nums[Math.abs(num)-1]<0){
// 已经转过一次负数的数, 表明重复
result[0]=Math.abs(num);
}else{
// 将所有元素转为负数
nums[Math.abs(num)-1]*=-1;
}
}
for(inti=1;i
// 剩下那个没有被转成负数的数
if(nums[i]>0){
result[1]=i+1;
}
}
returnresult;
}

07 第六种解法

此解法是使用异或运算, 来自讨论区. 传送门:publicint[]findErrorNums(int[]nums){

int[]ans=newint[2];
for(inti=0;i
intval=Math.abs(nums[i]);
ans[1]^=(i+1)^val;
if(nums[val-1]<0)ans[0]=val;
elsenums[val-1]=-nums[val-1];
}
ans[1]^=ans[0];
returnans;
}

08 小结