您现在的位置是:首页 >技术杂谈 >哈希表题目:缺失的第一个正数网站首页技术杂谈
哈希表题目:缺失的第一个正数
题目
标题和出处
标题:缺失的第一个正数
出处:41. 缺失的第一个正数
难度
7 级
题目描述
要求
给你一个未排序的整数数组 nums exttt{nums} nums,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) exttt{O(n)} O(n) 并且只使用常数级别额外空间的解决方案。
示例
示例 1:
输入:
nums
=
[1,2,0]
exttt{nums = [1,2,0]}
nums = [1,2,0]
输出:
3
exttt{3}
3
示例 2:
输入:
nums
=
[3,4,-1,1]
exttt{nums = [3,4,-1,1]}
nums = [3,4,-1,1]
输出:
2
exttt{2}
2
示例 3:
输入:
nums
=
[7,8,9,11,12]
exttt{nums = [7,8,9,11,12]}
nums = [7,8,9,11,12]
输出:
1
exttt{1}
1
数据范围
- 1 ≤ nums.length ≤ 5 × 10 5 exttt{1} le exttt{nums.length} le exttt{5} imes exttt{10}^ exttt{5} 1≤nums.length≤5×105
- -2 31 ≤ nums[i] ≤ 2 31 − 1 exttt{-2}^ exttt{31} le exttt{nums[i]} le exttt{2}^ exttt{31} - exttt{1} -231≤nums[i]≤231−1
解法
思路和算法
这道题要求找出数组 nums extit{nums} nums 中缺失的第一个正整数。假设数组 nums extit{nums} nums 的长度是 n n n,常规的做法是使用哈希集合存储数组中的整数,然后遍历从 1 1 1 到 n n n 的每个正整数,当遇到第一个不在哈希表中的正整数时返回该正整数,如果从 1 1 1 到 n n n 的正整数都在哈希集合中则数组的 n n n 个元素为这 n n n 个不同的正整数,返回 n + 1 n + 1 n+1。使用哈希表的时间复杂度是 O ( n ) O(n) O(n),符合题目要求,但是空间复杂度是 O ( n ) O(n) O(n),不符合题目要求的常数空间。
为了将空间复杂度降低到常数,不能额外创建哈希表,只能原地修改数组。由于数组 nums extit{nums} nums 的长度是 n n n,下标范围是 [ 0 , n − 1 ] [0, n - 1] [0,n−1],因此可以将数组看成哈希表,利用数组下标的信息表示每个正整数是否在数组中。对于下标 index extit{index} index,满足 0 ≤ index < n 0 le extit{index} < n 0≤index<n, 1 ≤ index + 1 ≤ n 1 le extit{index} + 1 le n 1≤index+1≤n, nums [ index ] extit{nums}[ extit{index}] nums[index] 可以用于表示正整数 index + 1 extit{index} + 1 index+1 是否在数组中。
由于数组中的元素可能是任意整数,不一定在范围 [ 1 , n ] [1, n] [1,n] 内,也可能是 0 0 0、负整数或者大于 n n n 的正整数,因此需要处理数组中的元素,使得数组中的元素都在范围 [ 1 , n ] [1, n] [1,n] 内且不影响结果。
由于 1 1 1 是最小的正整数,因此可以首先遍历数组判断 1 1 1 是否在数组中,如果 1 1 1 不在数组中则缺失的第一个正整数是 1 1 1,返回 1 1 1,如果 1 1 1 在数组中则继续寻找缺失的第一个正整数。
当 1 1 1 在数组中时,缺失的第一个正整数一定大于 1 1 1,因此可以将数组中所有的小于 1 1 1 或者大于 n n n 的元素全部改成 1 1 1,使得所有元素都在范围 [ 1 , n ] [1, n] [1,n] 内,然后利用数组下标的信息寻找缺失的第一个正整数。
遍历数组 nums extit{nums} nums。对于元素 num extit{num} num,其对应的下标为 index = ∣ num ∣ − 1 extit{index} = | extit{num}| - 1 index=∣num∣−1,如果 nums [ index ] > 0 extit{nums}[ extit{index}] > 0 nums[index]>0,则将 nums [ index ] extit{nums}[ extit{index}] nums[index] 的值更新为其相反数。遍历结束之后,对于 0 ≤ index < n 0 le extit{index} < n 0≤index<n, nums [ index ] < 0 extit{nums}[ extit{index}] < 0 nums[index]<0 表示正整数 index + 1 extit{index} + 1 index+1 在数组 nums extit{nums} nums 中, nums [ index ] > 0 extit{nums}[ extit{index}] > 0 nums[index]>0 表示正整数 index + 1 extit{index} + 1 index+1 不在数组 nums extit{nums} nums 中。
再次遍历数组 nums extit{nums} nums,找到最小的满足 nums [ i ] > 0 extit{nums}[i] > 0 nums[i]>0 的下标 i i i 并返回 i + 1 i + 1 i+1。如果数组 nums extit{nums} nums 中的所有元素都是负数,则 1 1 1 到 n n n 都在数组中,返回 n + 1 n + 1 n+1。
上述操作对数组 nums extit{nums} nums 遍历了 4 4 4 次:
-
第 1 1 1 次遍历判断 1 1 1 是否在数组中;
-
第 2 2 2 次遍历将数组中所有的小于 1 1 1 或者大于 n n n 的元素全部改成 1 1 1;
-
第 3 3 3 次遍历将数组中的每个元素对应的下标处的元素更新为负数;
-
第 4 4 4 次遍历寻找缺失的第一个正整数。
其实,前两次遍历可以合并为一次遍历,具体做法是使用一个标志符记录 1 1 1 是否在数组中,标志符的初始值为 false ext{false} false,对于数组中的每个元素,如果该元素等于 1 1 1 则将标志符设为 true ext{true} true,如果该元素小于 1 1 1 或者大于 n n n 则将该元素改成 1 1 1。该次遍历结束时,如果标志符为 false ext{false} false 则返回 1 1 1,并不会因为数组中的部分元素被改成 1 1 1 而影响结果,如果标志符为 true ext{true} true 则继续寻找缺失的第一个正整数。
将前两次遍历合并为一次遍历之后,遍历总次数从 4 4 4 次减少为 3 3 3 次。
代码
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
boolean oneExists = false;
for (int i = 0; i < n; i++) {
if (nums[i] == 1) {
oneExists = true;
} else if (nums[i] < 1 || nums[i] > n) {
nums[i] = 1;
}
}
if (!oneExists) {
return 1;
}
for (int i = 0; i < n; i++) {
int num = nums[i];
int index = Math.abs(num) - 1;
if (nums[index] > 0) {
nums[index] = -nums[index];
}
}
for (int i = 0; i < n; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums extit{nums} nums 的长度。需要遍历数组 nums extit{nums} nums 三次,对于每个元素的操作时间都是 O ( 1 ) O(1) O(1)。
-
空间复杂度: O ( 1 ) O(1) O(1)。由于是原地修改数组,因此额外使用的空间是常数。