题目
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
进阶:你可以实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案吗?
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:输入:nums = [3,4,-1,1]
输出:2
示例 3:输入:nums = [7,8,9,11,12]
输出:1
提示:
0 <= nums.length <= 300
-231 <= nums[i] <= 231 - 1来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/first-missing-positive
思路
这个题的难点就在于要求时间复杂度为O(n),并且空间复杂度为常数级别。
O(n)的时间复杂度也就是要求常数次的遍历数组,空间复杂度为常数级别确实是很难满足。
一开始偷巧,根据提示的0<= nums.length <= 300,那么最终解的最小正整数一定在1-301之间,用一个长度为300的数组保留下已经出现过的1-300之间的数字,最后遍历找到没出现的最小正整数,这是常数级别的空间和时间。其实这里更灵活一些,数组长度可以用nums.length(更进一步,可以用Math.min(nums.length, 300))。代码如下
class Solution {
public int firstMissingPositive(int[] nums) {
boolean []showup = new boolean[301];
for (int data : nums) {
if (data > 0 && data <= 300) {
showup[data] = true;
}
}
for (int i = 1; i < showup.length; i++) {
if (!showup[i]) {
return i;
}
}
return 301;
}
}
但偷巧的方法毕竟不是正道。看了官方解法,发现原来还有一层灵活应用。那就是可以直接在原来的数组基础上进行修改,这个思路是可以延续偷巧的思路的,只是不需要新建一个数组了,而是在原来的数组基础上进行修改。
修改的方法比较容易想到的一个就是:用索引和数字一一对应(nums[i] = i + 1),如果当前遍历到的数字不满足这个关系,就将当前数字放到它该放的地方去,代码如下:
class Solution {
public int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int data = nums[i];
// 如果数字在[0,nums.length]之间,且data不在它应该在的位置,就进行交换
if (data > 0 && data <= nums.length && data != (i + 1)) {
nums[i] = nums[data - 1];
nums[data - 1] = data;
if (data != nums[i]) { // 这里是为了避免换回来的数字和原来一样,会死循环
i--;
}
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.length + 1;
}
}
官方解法里还给了另一种思路,也是非常巧妙,不需要数字置换,而是通过给指定索引处加标记的方式。那么怎么加标记呢?首先把数组中所有不在[1,nums.length]之间的数组全部替换成nums.length+1,这样所有的数字就都是正数了。然后就可以通过给指定索引加负号的形式加标记。最后数组中所有带负号的位置i就代表原始数组中有 i+1这个数字。遍历数组,找到第一个不带负号的k,返回k+1即可。代码如下:
class Solution {
public int firstMissingPositive(int[] nums) {
// 先把所有不符合条件的数字都替换成nums.length+1
for (int i = 0; i < nums.length; i++) {
if (nums[i] <= 0 || nums[i] > nums.length) {
nums[i] = nums.length + 1;
}
}
// 给符合条件的数字,在指定索引出加负号标记
for (int data : nums) {
data = Math.abs(data); // 因为可能有符号标记,所以取绝对值
if (data > 0 && data <= nums.length) {
nums[data - 1] = -Math.abs(nums[data - 1]); // 因为可能之前被加过标记,需要取绝对值
}
}
// 遍历找到第一个不带负号的位置
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return nums.length + 1;
}
}
耗时 50分钟