缺失的第一个正数
- https://leetcode.cn/problems/first-missing-positive/
描述
-
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
-
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1
输入:nums = [1,2,0]
输出:3
示例 2
输入:nums = [3,4,-1,1]
输出:2
示例 3
输入:nums = [7,8,9,11,12]
输出:1
Typescript 版算法实现
1 )一般思路
function firstMissingPositive(nums: number[]): number {
// 判断当前传入数组,如果为空,直接返回1
if (!nums.length) return 1;
// 过滤掉非正整数
nums = nums.filter(item => item > 0);
// 再次判断过滤后的数组,如果为空,直接返回1
if (!nums.length) return 1;
// 升序,目的:方便从左到右取最小值arr[0]
nums.sort((a: number, b: number) => a - b);
// 如果第一个元素不为1,返回1
if (nums[0] !== 1) return 1;
// 从左边开始遍历,只要下一个元素和当前元素差值 > 1说明当前元素的下一个值(+1)就是所求
const n: number = nums.length;
for (let i:number = 0; i < n - 1; i++) {
if (nums[i + 1] - nums[i] > 1) return nums[i] + 1;
}
// 如果数组是连续的正整数 [1,2,3,4,5,6]
return nums[n - 1] + 1;
}
- 这个算法不是最优解,做了很多不需要的事情,比如 filter 和 sort
- 不需要完全进行排序,如果数据量过大,将会有很大的浪费
- 找最小值,给整个数组排序是非必要的
2 )基于选择排序改造
function firstMissingPositive(nums: number[]): number {
// 判断当前传入数组,如果为空,直接返回1
if (!nums.length) return 1;
// 过滤掉非正整数
nums = nums.filter(item => item > 0);
const n: number = nums.length;
// 再次判断过滤后的数组,如果为空,直接返回1
if (!n) return 1;
let min: number;
// 实现选择排序,先拿到最小值,如果第一个元素不是1直接返回1,如果是1,就要比相邻元素差值
for (let i: number = 0; i <= n; i++) {
min = nums[i]; // 最小值取第一项元素
for (let j: number = i + 1; j <= n; j++) {
// 下一项如果小于当前的最小值,则交换最小值,得到最新最小值
if (nums[j] < min) [min, nums[j]] = [nums[j], min];
}
// 记录下当前轮的最小值
nums[i] = min;
// 下面是业务性判断, 遍历一次,但是最小值不是1,则返回最小正整数1
if (i === 0 && min !== 1) return 1;
// 遍历超过1轮,则对比前面的相邻两元素的差值
if (i > 0 && (nums[i] - nums[i - 1] > 1)) return nums[i - 1] + 1;
}
// 最终返回最后一项的值 + 1
return nums[n - 1] + 1;
}
- 这个场景,适合选择排序,因为使用选择排序,在第一轮循环就能拿到最小值
- 但是,在数据量较大的场景下,这个基于选择排序的查找, 在 leetcode 上会存在 超出时间限制 的问题
3 )基于Map哈希表来处理
function firstMissingPositive(nums: number[]): number {
if (!nums.length) return 1;
// 创建一个map结构存储字典,将正整数存储到
const m = new Map();
let max: number = -Infinity; // 最大值
// 一个循环中处理完毕
nums.forEach(item => {
// 非正整数跳出循环
if (item <= 0) return;
m.set(item, 1); // map 存储当前值
max = Math.max(max, item); // 找到当前最大值
});
let minPositive: number = 1;
while(minPositive <= max) {
if (!m.get(minPositive)) return minPositive;
minPositive ++;
}
return minPositive;
}
- 将所有正整数放入哈希表,随后从 1 开始依次枚举正整数直到最大值,并判断其是否在哈希表里面
- 时间复杂度为 O(n),空间复杂度为 O(n)
- 这个哈希map所需时间取决于数组的长度,空间复杂度不符合要求, 有额外的map空间
- 这是一个比较好的算法,也是官方提出的原始方案,但官方不推荐,是因为需求有空间的限定
- 还有另一种思路是 从 1 开始依次枚举正整数,并遍历数组,判断其是否在数组中
- 时间复杂度为 O( n 2 n^2 n2)
- 空间复杂度为 O(1)
- 这也是官方提出的思路,不写对应的相关代码了,也是不符合需求的
4 )负号标记法改造数组,替代Map,改造标记字典
function firstMissingPositive(nums: number[]): number {
const n: number = nums.length;
const { abs } = Math;
let i: number;
// 第一轮循环将所有的非正整数都修改成 n + 1, 这时候 数组内都是正整数了,方便加负号标记了
for (let i = 0; i < n; ++i) {
if (nums[i] <= 0) nums[i] = n + 1;
}
// 第二轮循环将当前元素中 1 ~ n 之内的数对应的位置上的值修改成负值,这样就标记了该位置表示的值(下标)已经出现过了
for (i = 0; i < n; ++i) {
let num = abs(nums[i]); // 拿到当前下标对应的元素的绝对值
if (num <= n) nums[num - 1] = -abs(nums[num - 1]);
}
// 最后一轮遍历是找数组中仍是正数的下标,该下标所在的位置就是所求
for (i = 0; i < n; ++i) {
if (nums[i] > 0) return i + 1;
}
// 以上运算都找不到正数,则都已经标记为负数,直接返回 n + 1
return n + 1;
}
- 官方提供方法
- 首次缺失的正数,一定在 [1, N + 1]范围内
- 数组本身有索引, 而每个索引都有对应的元素, 将数组的索引当做哈希的键值
- 在对应元素上添加特殊符号(负号),来标记该位数字是否存在,负号表示存在
- 比如1出现了,把第一个元素变成负数,2出现了,把第二个元素(索引值为1的元素)变成负数
- 只要一个数字出现,它对应的第几个元素就标记为负数
- 只要返回第一个正数的位置,就是缺失的第一个正数
- 时间复杂度:O(N),其中 N 是数组的长度
- 空间复杂度:O(1)
5 )置换法
function firstMissingPositive(nums: number[]): number {
const n: number = nums.length;
let i: number;
// 第一轮遍历
for (i = 0; i < n; ++i) {
// 如果当前元素在 0 ~ n 之间, 当前元素 和 当前元素对应的位置 - 1 (下标) 不一致 则进行交换
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
[nums[nums[i] - 1], nums[i]] = [nums[i], nums[nums[i] - 1]]; // 交换
}
}
// 第二轮遍历,找到第一个当前值与当前位置不匹配时的位置(备注: 位置比下标大1)
for (i = 0; i < n; ++i) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return n + 1;
}
- 这是官方提供
- 将正确的元素放在正确的位置上,寻找第一个值与下标不匹配的元素
- 如果数组中包含 x∈[1,N],那么恢复后,数组的第 x−1 个元素为 x
- 在恢复后,数组应当有 [1, 2, …, N] 的形式,但其中有若干个位置上的数是错误的,每一个错误的位置就代表了一个缺失的正数
- 以题目中的示例二 [3, 4, -1, 1] 为例,恢复后的数组应当为 [1, -1, 3, 4],我们就可以知道缺失的数为 2
- 时间复杂度:O(N),其中 N 是数组的长度
- 空间复杂度:O(1)
Python3 版算法实现
1 )方案1
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
# 判断当前传入数组,如果为空,直接返回1
if not nums:
return 1
# 过滤掉非正整数
nums = [item for item in nums if item > 0]
# 再次判断过滤后的数组,如果为空,直接返回1
if not nums:
return 1
# 升序,目的:方便从左到右取最小值arr[0]
nums.sort()
# 如果第一个元素不为1,返回1
if nums[0] != 1:
return 1
# 从左边开始遍历,只要下一个元素和当前元素差值 > 1说明当前元素的下一个值(+1)就是所求
n = len(nums)
for i in range(n - 1):
if nums[i + 1] - nums[i] > 1:
return nums[i] + 1
# 如果数组是连续的正整数 [1,2,3,4,5,6]
return nums[n - 1] + 1
2 )方案2
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
# 判断当前传入数组,如果为空,直接返回1
if not nums:
return 1
# 过滤掉非正整数
nums = [item for item in nums if item > 0]
n = len(nums)
# 再次判断过滤后的数组,如果为空,直接返回1
if not n:
return 1
min_val = None
# 实现选择排序,先拿到最小值,如果第一个元素不是1直接返回1,如果是1,就要比相邻元素差值
for i in range(n):
min_val = nums[i] # 最小值取第一项元素
for j in range(i + 1, n):
# 下一项如果小于当前的最小值,则交换最小值,得到最新最小值
if nums[j] < min_val:
min_val, nums[j] = nums[j], min_val
# 记录下当前轮的最小值
nums[i] = min_val
# 下面是业务性判断, 遍历一次,但是最小值不是1,则返回最小正整数1
if i == 0 and min_val != 1:
return 1
# 遍历超过1轮,则对比前面的相邻两元素的差值
if i > 0 and (nums[i] - nums[i - 1] > 1):
return nums[i - 1] + 1
# 最终返回最后一项的值 + 1
return nums[n - 1] + 1
- Leetcode 上超出时间限制
3 )方案3
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
if not nums:
return 1
# 创建一个map结构存储字典,将正整数存储到
m = {}
max_val = float('-inf') # 最大值
# 一个循环中处理完毕
for item in nums:
# 非正整数跳出循环
if item <= 0:
continue
m[item] = 1 # map 存储当前值
max_val = max(max_val, item) # 找到当前最大值
min_positive = 1
while min_positive <= max_val:
if min_positive not in m:
return min_positive
min_positive += 1
return min_positive
4 )方案4
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
# 第一轮循环将所有的非正整数都修改成 n + 1, 这时候 数组内都是正整数了,方便加负号标记了
for i in range(n):
if nums[i] <= 0:
nums[i] = n + 1
# 第二轮循环将当前元素中 1 ~ n 之内的数对应的位置上的值修改成负值,这样就标记了该位置表示的值(下标)已经出现过了
for i in range(n):
num = abs(nums[i]) # 拿到当前下标对应的元素的绝对值
if num <= n:
nums[num - 1] = -abs(nums[num - 1])
# 最后一轮遍历是找数组中仍是正数的下标,该下标所在的位置就是所求
for i in range(n):
if nums[i] > 0:
return i + 1
# 以上运算都找不到正数,则都已经标记为负数,直接返回 n + 1
return n + 1
5 )方案5
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
# 第一轮遍历
for i in range(n):
# 如果当前元素在 0 ~ n 之间, 当前元素 和 当前元素对应的位置 - 1 (下标) 不一致 则进行交换
while 0 < nums[i] <= n and nums[nums[i] - 1] != nums[i]:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1] # 交换
# 第二轮遍历,找到第一个当前值与当前位置不匹配时的位置(备注: 位置比下标大1)
for i in range(n):
if nums[i] != i + 1:
return i + 1
return n + 1
Golang 版算法实现
1 )方案1
func firstMissingPositive(nums []int) int {
// 判断当前传入数组,如果为空,直接返回1
if len(nums) == 0 {
return 1
}
// 过滤掉非正整数
filteredNums := []int{}
for _, item := range nums {
if item > 0 {
filteredNums = append(filteredNums, item)
}
}
// 再次判断过滤后的数组,如果为空,直接返回1
if len(filteredNums) == 0 {
return 1
}
// 升序,目的:方便从左到右取最小值arr[0]
sort.Ints(filteredNums)
// 如果第一个元素不为1,返回1
if filteredNums[0] != 1 {
return 1
}
// 从左边开始遍历,只要下一个元素和当前元素差值 > 1说明当前元素的下一个值(+1)就是所求
n := len(filteredNums)
for i := 0; i < n-1; i++ {
if filteredNums[i+1]-filteredNums[i] > 1 {
return filteredNums[i] + 1
}
}
// 如果数组是连续的正整数 [1,2,3,4,5,6]
return filteredNums[n-1] + 1
}
2 )方案2
func firstMissingPositive(nums []int) int {
// 判断当前传入数组,如果为空,直接返回1
if len(nums) == 0 {
return 1
}
// 过滤掉非正整数
filteredNums := []int{}
for _, item := range nums {
if item > 0 {
filteredNums = append(filteredNums, item)
}
}
n := len(filteredNums)
// 再次判断过滤后的数组,如果为空,直接返回1
if n == 0 {
return 1
}
var min int
// 实现选择排序,先拿到最小值,如果第一个元素不是1直接返回1,如果是1,就要比相邻元素差值
for i := 0; i < n; i++ {
min = filteredNums[i] // 最小值取第一项元素
for j := i + 1; j < n; j++ {
// 下一项如果小于当前的最小值,则交换最小值,得到最新最小值
if filteredNums[j] < min {
min, filteredNums[j] = filteredNums[j], min
}
}
// 记录下当前轮的最小值
filteredNums[i] = min
// 下面是业务性判断, 遍历一次,但是最小值不是1,则返回最小正整数1
if i == 0 && min != 1 {
return 1
}
// 遍历超过1轮,则对比前面的相邻两元素的差值
if i > 0 && (filteredNums[i] - filteredNums[i-1] > 1) {
return filteredNums[i-1] + 1
}
}
// 最终返回最后一项的值 + 1
return filteredNums[n-1] + 1
}
- LeetCode 上超出时间限制
3 )方案3
func firstMissingPositive(nums []int) int {
if len(nums) == 0 {
return 1
}
// 创建一个map结构存储字典,将正整数存储到
m := make(map[int]bool)
maxVal := -1 << 31 // 最大值
// 一个循环中处理完毕
for _, item := range nums {
// 非正整数跳出循环
if item <= 0 {
continue
}
m[item] = true // map 存储当前值
maxVal = max(maxVal, item) // 找到当前最大值
}
minPositive := 1
for minPositive <= maxVal {
if !m[minPositive] {
return minPositive
}
minPositive++
}
return minPositive
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
4 )方案4
import "math"
func firstMissingPositive(nums []int) int {
n := len(nums)
// 第一轮循环将所有的非正整数都修改成 n + 1, 这时候 数组内都是正整数了,方便加负号标记了
for i := 0; i < n; i++ {
if nums[i] <= 0 {
nums[i] = n + 1
}
}
// 第二轮循环将当前元素中 1 ~ n 之内的数对应的位置上的值修改成负值,这样就标记了该位置表示的值(下标)已经出现过了
for i := 0; i < n; i++ {
num := int(math.Abs(float64(nums[i]))) // 拿到当前下标对应的元素的绝对值
if num <= n {
nums[num-1] = int(math.Abs(float64(nums[num-1])) * -1)
}
}
// 最后一轮遍历是找数组中仍是正数的下标,该下标所在的位置就是所求
for i := 0; i < n; i++ {
if nums[i] > 0 {
return i + 1
}
}
// 以上运算都找不到正数,则都已经标记为负数,直接返回 n + 1
return n + 1
}
5 )方案5
func firstMissingPositive(nums []int) int {
n := len(nums)
// 第一轮遍历
for i := 0; i < n; i++ {
// 如果当前元素在 0 ~ n 之间, 当前元素 和 当前元素对应的位置 - 1 (下标) 不一致 则进行交换
for nums[i] > 0 && nums[i] <= n && nums[nums[i]-1] != nums[i] {
nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1] // 交换
}
}
// 第二轮遍历,找到第一个当前值与当前位置不匹配时的位置(备注: 位置比下标大1)
for i := 0; i < n; i++ {
if nums[i] != i+1 {
return i + 1
}
}
return n + 1
}