2025/7/13
题目(hard):
我的思路:
我想的是先给他排序,然后再从第一个大于0并且和前一个元素的值不同的元素开始一个个从1开始进行枚举,直到枚举值和元素值不相同的时候就返回这个枚举值作为结果
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
res = 1
pre = 0
nums.sort() #1.排序数组
for num in nums:
if num <= 0 or num == pre: #如果当前值小于0,或者当前值和它的上一个值一样的就直接跳过
continue
else:
if res != num: #如果本该有的正整数在数组里没有就返回这个值了
return res
else:
res+=1 #这个正整数已经在了,所以+1检查下一个正整数
pre = num
return res
时间复杂度:O(nlogn) 【Python内置的排序算法是Timsort算法,平均和最坏时间复杂度是nlogn】
空间复杂度:O(n) 【Python内置排序算法最坏需要的空间是n】
写完才发现好像不是很符合题目的要求:时间复杂度O(n),空间复杂度O(1)
优化思路:
1.使用哈希表
如果不要求空间复杂度O(1)的话,而且又只是逐个枚举从1开始的正整数看它是否在数组中,那自然可以想到使用哈希表来处理这个问题
代码如下:
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
res = 1
hash_set = set()
for num in nums: #1.数组存入哈希表中
hash_set.add(num)
while True:
if res not in hash_set: #枚举看正整数是否在哈希表里
return res
else:
res += 1
时间复杂度:O(n)
空间复杂度:O(n)
不过这个显然还是不满足空间复杂度为常数级的要求
2.打标记
我们之所以用哈希表来存储时因为哈希表可以帮助我们快速查找出这个值是否存在数组中。不过既然要用O(1)的空间复杂度,那就只能考虑在数组本身上做手脚,考虑把它做成哈希表类似物。
观察可以发现,若数组长度为N,则答案要么在[1,N]之间,要么是N+1,所以如果我们可以把已经在数组中的正整数进行标记,然后逐个按索引遍历的时候,发现没有标记的位置,就返回这个【索引值+1】就是我们需要的答案了
因此我们关心的问题是:如何对对应位置的元素进行标记呢?
最好的标记肯定需要满足两个条件:
①可以认出这是个标记
②可以保留原始的信息,方便后续比对
而我们知道,负数是肯定不会是答案的,所以如果我们把这个数标记为负数,那既可以知道这个数被标记了,又可以在需要的时候重新获取这个数的值(把它取个绝对值就好了,比如4被标记成了-4,我们可以知道它被标记过了,同时也可以知道它是4)
那接下来还有一个问题,就是原数组中已经有的负数要怎么处理呢?
我们可以考虑把它改成任意[1,N]以外的数字,这样它们就不会干扰我们对现有的索引位置进行标记,这里我们把它们都处理成N+1即可
综上所述,我们的算法步骤可以总结成
①第一次遍历把所有负数改成N+1
②第二次遍历把以当前数字num的num-1索引位置的元素标记为负数
③第三次遍历返回第一个不为负数的索引值+1即可
④若③中遍历完发现都已经被标记为负数,那答案就是N+1了(否则不可能标记完)
代码如下:
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(0,n): #1.把所有负数打成正数
if nums[i] <= 0:
nums[i] = n+1
for num in nums: #2.给所有已有的正整数num,在num-1的位置打标(打标为负数,这样可以保留原来的值的信息)
index = abs(num)-1
if index >= n:
continue
elif nums[index] > 0:
nums[index] = -nums[index]
for i in range(0,n): #3.查找没有打标为负数的位置,返回这个位置+1就是答案
if nums[i] > 0:
return i+1
return n+1 #4.如果都打标了,那答案只能是数组长度+1了(n+1)
时间复杂度:O(n)
空间复杂度:O(1)
这里最巧妙的地方在于给已经在数组中的正整数值-1的索引位置的值打标为负数,这样在之后的遍历中还是可以正常取出相应的数值
3.一个萝卜一个坑🕳(恢复排序)
通过上面的分析我们知道了答案要么是[1,N]中的某一个数,要么是N+1。所以如果我们把这个数组中的数字,按其值排到其应该在的索引位置来进行恢复排序,一个萝卜一个坑🕳,那肯定是可以一眼看出来哪一个坑🕳里第一个没有放入正确的萝卜的。
比如[1,4,-1,3]恢复排序后应该是[1,-1,3,4],可以看出来2这个坑里没有2这个萝卜
或者[0,2,2,1,1,3]这种,恢复排序后是[1,2,3,x,x,x],我们只关心正确的在范围内的萝卜[1,N],在哪个坑位,至于相同的萝卜或者没有坑位的萝卜就随便往后放置了,因为它们没有坑也没有相应的萝卜来占住这个坑位,所以它们也没必要挪开。
代码如下:
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
#1.给数组恢复排序
n = len(nums)
for i in range(0,n):
x = nums[i]
while 1<= x <= n and x != nums[x-1]: #2.把元素移到相应的位置x-1,每次从x-1位置挪回来的元素可能也能继续挪
nums[i],nums[x-1] = nums[x-1],nums[i]
x = nums[i]
for i in range(0,n): #3.遍历搜索第一个没有放置对应元素的索引下标,返回索引值+1
if i+1 != nums[i]:
return i+1
return n+1 #4.数组中所有索引值都有对应的元素值,返回数组长度+1
时间复杂度:O(n)
空间复杂度:O(1)
总结:
①对于要求空间复杂度为O(1)的,我们只能考虑在给定的数组本身进行操作,并尽可能地在其中记录更多需要的信息
②观察答案可能出现的范围与数组的长度等信息之间的关联,就可以更好地利用数组本身来进行求解。比如这题中观察到答案范围在[1,N]中,否则就是N+1这个信息十分重要