题目
给你一个含 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
进阶:你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。
答案
#include <vector>
#include <cmath>
class Solution {
public:
std::vector<int> findDisappearedNumbers(std::vector<int>& nums) {
std::vector<int> result;
int n = nums.size();
// 将出现过的数字对应的索引位置上的元素取负值
for (int i = 0; i < n; ++i) {
int index = abs(nums[i]) - 1;
if (nums[index] > 0) {
nums[index] = -nums[index];
}
}
// 找出那些仍然是正数的索引位置,这些就是消失的数字
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
result.push_back(i + 1);
}
}
return result;
}
};
算法原理
这个算法的核心思想是利用数组本身来存储一个额外的信息:
即某个数字是否出现过。由于题目中给出的数字范围是 [1, n](其中 n 是数组的长度),并且我们知道数组中的元素都是这个范围内的整数(尽管可能有重复),我们可以利用数组索引来间接地“标记”这些数字。
- 索引作为标记位置:
对于数组中的每个元素nums[i]
,我们可以将其绝对值视为一个有效索引(index = abs(nums[i]) - 1)
,这个索引对应于我们想要跟踪的数字(即 index + 1)。由于数组索引是从 0 开始的,而我们的数字范围是从 1 开始的,所以我们需要从 nums[i] 的值中减去 1 来得到正确的索引。 - 利用数组元素的正负性作为标记:
数组 nums 中的每个元素原本都是一个整数。我们可以利用这些整数的正负性来标记对应的数字是否出现过。具体做法是:如果nums[index]
是正数,说明数字 index + 1 还没有被标记过(即还没有出现过),我们就将其取负值来标记它。如果nums[index]
已经是负数,说明数字 index + 1 已经被标记过了(可能是之前就已经在数组中出现过,或者是在遍历过程中被重复标记了),此时我们不需要做任何操作。 - 第二次遍历找出缺失的数字:
经过第一次遍历后,数组 nums 中的每个元素都已经被用来标记了对应的数字是否出现过。具体来说,如果 nums[i] 是正数,那么说明数字 i + 1 没有在数组中出现过,是一个缺失的数字。因此,我们只需要再次遍历数组 nums,找出所有正数对应的索引,并将这些索引加 1 后添加到结果数组中。 - 算法的正确性:
这个算法之所以有效,是因为它巧妙地利用了数组索引和元素值之间的对应关系。通过修改数组元素的正负性来标记数字是否出现过,我们避免了使用额外的数据结构来存储这个信息。此外,由于数字的范围和数组的长度相同,我们可以确信每个数字都有一个对应的索引位置来存储其是否出现过的信息。 - 算法的限制:
需要注意的是,这个算法会修改原数组 nums。如果原数组不能被修改,或者需要保留原数组的内容,那么就不能使用这个算法。在这种情况下,可以考虑使用额外的数据结构(如哈希表或布尔数组)来存储每个数字是否出现过的信息。 - 算法的效率:
这个算法的时间复杂度是 O(n),因为我们只遍历了数组两次。空间复杂度是 O(1)(不考虑输出结果的空间),因为我们只使用了常数个额外变量,并且直接在原数组上进行了修改来存储额外的信息。这使得这个算法在空间使用上非常高效。