题目:
给你一个含
n
个整数的数组nums
,其中nums[i]
在区间[1, n]
内。请你找出所有在[1, n]
范围内但没有出现在nums
中的数字,并以数组的形式返回结果。示例 1:
输入:nums = [4,3,2,7,8,2,3,1] 输出:[5,6]示例 2:
输入:nums = [1,1] 输出:[2]提示:
n == nums.length
1 <= n <= 105
1 <= nums[i] <= n
暴力算法解题思路
暴力算法的思路是遍历整数范围 [1, n],对于每一个整数检查其是否在数组 nums中出现。如果某个整数没有出现在 nums中,就将其添加到结果数组中。
具体步骤如下:
创建一个布尔数组 present,用来标记范围 [1, n] 内的整数是否出现在 nums中。初始时,所有元素都设为 false。
第一次遍历 nums数组,将出现的元素在 present数组中对应位置设为 true。
第二次遍历 present数组,找到所有值为 false的索引,即为没有出现在 nums中的整数。
将这些整数添加到结果数组中并返回。
复杂度分析
时间复杂度:O(n)。需要两次遍历数组 nums,以及一次遍历长度为 n 的 present数组。这三次遍历的总时间复杂度为 O(n)。
空间复杂度:O(n)。额外使用了一个长度为 n 的布尔数组 present。
暴力算法实现示例(Java):
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> result = new ArrayList<>();
int n = nums.length;
// Step 1: 创建一个boolean类型的数组,因为数组下标从0开始
//所以我们创建数组长度为n+1 这样下标从0~n
boolean[] present = new boolean[n + 1];
// Step 2: 遍历nums数组,将出现的元素在 present数组中对应位置设为 true。
for (int num : nums) {
present[num] = true;
}
// Step 3: 找到从1~n没有出现的数字
for (int i = 1; i <= n; i++) {
if (!present[i]) {
result.add(i);
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums1 = {4, 3, 2, 7, 8, 2, 3, 1};
System.out.println(solution.findDisappearedNumbers(nums1));
int[] nums2 = {1, 1};
System.out.println(solution.findDisappearedNumbers(nums2));
}
}
进阶:
你能在不使用额外空间且时间复杂度为 O(n)
的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。
不使用额外的空间,那就只能利用数组本身的特性
方法一:将每个元素-1,结合下标处理
转换数组元素:
对于数组中的每个元素 nums[i],将其减去 1,得到 index = nums[i] - 1。这个 index就是数组中的下标。
标记出现情况:
如果 nums[index]大于 0,则将其乘以 -1(即取负),表示在原位置 index上的元素已经出现过。
如果 nums[index]已经是负数(即已经被标记过出现),则不做处理。
找出未出现的元素:
第二次遍历数组 nums,找出所有正数的下标 i,表示原数组中缺失的元素为 i + 1。
示例(Java)
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> result = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
int index = Math.abs(nums[i]) - 1;
/*当数组中存在重复元素时,例如数组中有两个元素值相同的情况,例如 [1, 2, 2]。在第一次遍历时,第一个 2可能会将下标为 1的元素标记为负数,然后第二个 2又会尝试标记同样的位置,这样做会使得原本出现的元素被误判为没有出现过,所以先判断*/
if (nums[index] > 0) {
nums[index] = -nums[index];
}
}
for (int i = 0; i < n; i++) {
if (nums[i] > 0) {
result.add(i + 1);
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums1 = {4, 3, 2, 7, 8, 2, 3, 1};
System.out.println(solution.findDisappearedNumbers(nums1)); // Output: [5, 6]
int[] nums2 = {1, 1};
System.out.println(solution.findDisappearedNumbers(nums2)); // Output: [2]
}
}
复杂度分析
-
时间复杂度:O(n),两次遍历数组。
-
空间复杂度:O(1),除了输出结果外,没有使用额外空间。
这种方法利用了原数组的特性,通过改变数组元素的正负来标记出现情况,从而在常数空间内找出缺失的元素,非常高效。
方法二:对每个元素-1进行取模运算后得到下标
初始化和第一次标记:
length
是数组nums
的长度,即元素个数。- 对于数组中的每个元素
nums[i]
,计算出应该标记的位置id
。这里使用(nums[i] - 1) % length
是为了将元素值转换成对应的下标(从0开始),这样可以在不修改原始数组的情况下进行标记。- 将
nums[id]
的值加上length
。这一步的目的是利用数组本身来标记数字的出现:如果数字i+1
存在于nums
中,那么nums[i]
的值会大于length
。第二次遍历找到缺失的数字:
- 创建一个空的
ArrayList
用来存放缺失的数字。- 再次遍历数组
nums
,对于每个下标i
,如果nums[i]
小于等于length
,则说明i+1
这个数字在nums
中没有出现,因为如果出现过,nums[i]
的值会被设置为大于length
。
示例(Java)
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
//利用下标
for (int i = 0; i < n; i++) {
//对n取模还原出原来的值对应的下标
int id = (nums[i] - 1 )% n;
//然后将该下标对应的值 + n
nums[id] += n;
}
List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if(nums[i] <= n){
result.add(i+1);
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums1 = {4, 3, 2, 7, 8, 2, 3, 1};
System.out.println(solution.findDisappearedNumbers(nums1));
int[] nums2 = {1, 1};
System.out.println(solution.findDisappearedNumbers(nums2));
}
}
复杂度分析
-
时间复杂度:O(n),其中 n 是数组
nums
的长度。算法需要对nums
进行两次线性扫描:一次是标记操作,一次是找到缺失数字的操作,因此总体时间复杂度为 O(2n),即 O(n)。 -
空间复杂度:O(1)。除了返回的结果数组外,算法没有使用额外的空间,所有操作均在原数组上进行修改和计算。
这种方法利用了原数组的特性,通过改变数组元素的大小,又通过取模运算还原出原来的值,最后通过大小来标记出现情况,从而在常数空间内找出缺失的元素,非常高效。