数据结构与算法之排序: LeetCode 41. 缺失的第一个正数 (Ts, Py, Go版)

文章讨论了在未排序整数数组中找到缺失的最小正整数的算法,包括一般思路、选择排序改造、基于哈希表、负号标记法和置换法。着重于实现时间复杂度为O(n)且空间复杂度低的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

缺失的第一个正数

  • 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
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值