「力扣」第 41 题:“缺失的第一个正数” 问题描述
给定一个未排序的整数数组,找出其中没有出现的最小的正整数。
示例 1:
输入: [1, 2, 0]
输出: 3
示例 2:
输入: [3, 4, -1, 1]
输出: 2
示例 3:
输入: [7, 8, 9, 11, 12]
输出: 1
说明:
你的算法的时间复杂度应为 ,并且只能使用常数级别的空间。
方法一:哈希表(空间复杂度不符合要求)
-
按照刚才我们读例子的思路,其实我们只需从最小的正整数 开始,依次判断 、 、 直到数组的长度
N
是否在数组中; -
如果当前考虑的数不在这个数组中,我们就找到了这个缺失的最小正整数;
-
由于我们需要依次判断某一个正整数是否在这个数组里,我们可以先把这个数组中所有的元素放进哈希表。接下来再遍历的时候,就可以以 的时间复杂度判断某个正整数是否在这个数组;
-
由于题目要求我们只能使用常数级别的空间,而哈希表的大小与数组的长度是线性相关的,因此空间复杂度不符合题目要求。
Java 代码:
import java.util.HashSet;
import java.util.Set;
public class Solution {
public int firstMissingPositive(int[] nums) {
int len = nums.length;
Set<Integer> hashSet = new HashSet<>(len);
for (int num : nums) {
hashSet.add(num);
}
for (int i = 1; i <= len ; i++) {
if (!hashSet.contains(i)){
return i;
}
}
return len + 1;
}
}
复杂度分析:
- 时间复杂度: , 是数组的长度;
- 空间复杂度: ,不符合题目的要求。
方法二:二分查找(时间复杂度不符合要求)
-
根据刚才的分析,这个问题其实就是要我们查找一个元素,而查找一个元素,如果是在有序数组中查找,会快一些;
-
因此我们可以将数组先排序,再使用二分查找法从最小的正整数 开始查找,找不到就返回这个正整数;
-
这个思路需要先对数组排序,而排序使用的时间复杂度是 ,是不符合这个问题的时间复杂度要求。
Java 代码:
import java.util.Arrays;
public class Solution {
public int firstMissingPositive(int[] nums) {
int len = nums.length;
Arrays.sort(nums);
for (int i = 1; i <= len; i++) {
int res = binarySearch(nums, i);
if (res == -1) {
return i;
}
}
return len + 1;
}
private int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
}
复杂度分析:
- 时间复杂度: ,这个算法的时间复杂度主要集中在排序算法的时间复杂度上,是不符合题目要求的;
- 空间复杂度: 。
方法三:将数组视为哈希表
- 由于题目要求我们“只能使用常数级别的空间”,而要找的数一定在
[1, N + 1]
左闭右闭(这里N
是数组的长度)这个区间里。因此,我们可以就把原始的数组当做哈希表来使用。事实上,哈希表其实本身也是一个数组; - 我们要找的数就在
[1, N + 1]
里,最后N + 1
这个元素我们不用找。因为在前面的N
个元素都找不到的情况下,我们才返回N + 1
; - 那么,我们可以采取这样的思路:就把 这个数放到下标为 的位置, 这个数放到下标为 的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
- 这个思想就相当于我们自己编写哈希函数,这个哈希函数的规则特别简单,那就是数值为
i
的数映射到下标为i - 1
的位置。
我们来看一下这个算法是如何应用在示例 2 上的。
-
我们一眼就可以看出来,最后那个数组,看起来最不顺眼的那个位置就是下标为
1
的位置,它应该放置的是数值 ,因此它就是缺失的第一个正数就是 ; -
我们在编码的时候,需要从左到右做一次扫描,来找到这个数。
这个思路是比较典型的:思路简单,但是编码并没有那么容易的问题。
Java 代码:
public class Solution {
public int firstMissingPositive(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
// 满足在指定范围内、并且没有放在正确的位置上,才交换
// 例如:数值 3 应该放在下标 2 的位置上
swap(nums, nums[i] - 1, i);
}
}
// [1, -1, 3, 4]
for (int i = 0; i < len; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 都正确则返回数组长度 + 1
return len + 1;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
public static void main(String[] args) {
int[] nums = {4, 3, 2, 1};
Solution solution = new Solution();
int firstMissingPositive = solution.firstMissingPositive(nums);
System.out.println(firstMissingPositive);
}
}
特别说明:被交换过来的那个数,我们只是判断了它不等于 nums[i]
,但是它应该放在哪一个位置,还需要继续做判断。因此 for
循环里应该写 while
。
复杂度分析:
- 时间复杂度: ,这里 是数组的长度。
说明:while
循环不会每一次都把数组里面的所有元素都看一遍。如果有一些元素在这一次的循环中被交换到了它们应该在的位置,那么在后续的遍历中,由于它们已经在正确的位置上了,代码再执行到它们的时候,就会被跳过。
最极端的一种情况是,在第 1 个位置经过这个 while
就把所有的元素都看了一遍,这个所有的元素都被放置在它们应该在的位置,那么 for
循环后面的部分的 while
的循环体都不会被执行。
平均下来,每个数只需要看一次就可以了,while
循环体被执行很多次的情况不会每次都发生。这样的复杂度分析的方法叫做均摊复杂度分析。
- 空间复杂度: 。