这里是题目描述:LeetCode-41.缺失的第一个正数
本题的难度标注为hard
。本题的难点在于解法需要同时满足 O(n) 的时间复杂度和常数级别的空间复杂度。
首先假设我们不去考虑空间复杂度的要求,那么如何在 O(n) 的时间开销下解决本题?对了,就是哈希表,像这种考察数组中数字是否存在、数字是否重复、数字重复次数的题,都可以考虑借助哈希表这种数据结构解题
借助哈希表的解法(不满足空间限制)
首先要知道一个前提,假设给定的数组nums
的长度为n
,那么数组缺失的第一个正数就一定在1~n+1
这个范围内。我们建立一个哈希表,遍历数组nums
,将数组中所有的数字都存入哈希表;然后再遍历数字1~n+1
,第一个不存在于哈希表中的数字,就是nums
中缺失的第一个正数,将该数字返回
本算法的时间复杂度为O(n),空间复杂度为O(n),不满足题干中的空间复杂度限制。
原地哈希解法
因为需要建立并维护一个哈希表,因此会超出时间复杂度,我们可以使用题干所给的数组nums
来实现 “原地哈希” ,以满足空间复杂度限制。哈希表的本质其实也是数组,可以不另外建立哈希表而使用数组nums
来实现哈希表的功能。基于“原地哈希”的解法参考了LeetCode官网的题解:原地哈希解法
同样适用“原地哈希”来解的题还有:LeetCode-442.数组中重复的数据 和 LeetCode-448.找到所有数组中消失的数字
下面是原地哈希解法的过程:
- 处理特殊情况
1.1 特殊情况1,数组nums
长度为0
。直接返回结果1
1.2 特殊情况2,数组nums
长度为1
。若nums[0]==1
,直接返回结果2
;若nums!=1
,直接返回结果1
- 若不是
nums
长度为0
或1
的特殊情况,则开始原地哈希。这里原地哈希的准则是:将nums
中1~n
的数字分别放在nums
的0~n-1
的下标上,也就是说,把数组nums
本身作为一个哈希表,哈希函数为f(nums[i])=nums[i]-1
。对数组nums
进行第一次遍历,通过原地哈希函数将遍历到的数字放入对应位置
1.1 若遍历到的数字小于等于0
或大于n
,则该数字超范围,跳过该数字
1.2 若遍历到的数字nums[i]
满足0<nums[i]<=n
则该数字没有超范围,将它和nums[nums[i]-1]
交换,它就放到了原地哈希表对应的位置;特别的,如果nums[nums[i]-1]==nums[i]
,则表示遇到了重复数,跳过该数字 - 第二次遍历数组
nums
,如果发现下标i
处的数字不是i+1
,则i+1
是确实的第一个正数,返回i+1
。如果遍历完数组所有的下标i
处的数字都是i+1
,则缺失的第一个数字是n+1
,返回n+1
原地哈希法题解代码:
class Solution {
//原地哈希法。
/* 设数组nums的长度为n,那么第一个缺失的正数肯定在1~n+1范围内
* 将数组nums本身作为一个哈希表,将nums中1~n的数字分别放在nums的0~n-1的下标上
* 最后检查nums,当有下标m没有被放入对应数字m+1,则m+1就是第一个缺失的数字
* 如果所有下标都放入了对应数字,则第一个缺失的数字是n+1 */
public int firstMissingPositive(int[] nums) {
if(nums.length==0) //特殊情况1
{
return 1;
}
if(nums.length==1) //特殊情况2
{
if(nums[0]==1)
{
return 2;
}
else
{
return 1;
}
}
int n=nums.length;
int index=0;
while(index<n)
{
int val=nums[index];
if(val==index+1 || val<=0 || val>n) //当前位置已经放入了对应数字或该数字超范围,跳过
{
index++;
}
else //数字在1~n+1范围且没有放在对应位置
{
if(nums[val-1]==val) //遇见了重复的数字
{
index++;
continue;
}
nums[index]=nums[val-1];
nums[val-1]=val;
}
}
for(int i=0;i<n;i++) //第二次遍历,检查没有被放入对应数字的下标位置
{
if(nums[i]!=i+1)
{
return i+1;
}
}
return n+1;
}
}
时间复杂度:O(n)
空间复杂度:O(1)