题目信息
给你一个未排序的整数数组
nums,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为O(n)并且只使用常数级别额外空间的解决方案。示例 1:
输入:nums = [1,2,0] 输出:3 解释:范围 [1,2] 中的数字都在数组中。示例 2:
输入:nums = [3,4,-1,1] 输出:2 解释:1 在数组中,但 2 没有。示例 3:
输入:nums = [7,8,9,11,12] 输出:1 解释:最小的正数 1 没有出现。提示:
1 <= nums.length <= 10^5-2^31 <= nums[i] <= 2^31 - 1
思考与自写代码
依旧是能写且可以通过题目考察,但不符合要求
这题的难点在于空间复杂度,我写的代码没有满足常数级别的空间要求。自写代码如下:
python
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
# 最小的正整数肯定是1
# 要求时间复杂度为O(N),所以这里不能进行排序. 排序的时复杂度O(Nlog N)高于(也就是慢于)O(N)
ans = 1
seen = set()
for num in nums: seen.add(num)
flag = True
while flag:
if ans in seen: ans += 1
else: flag = False
return ans
标准代码
先要想明白一个事情 / 发现一个边界:长度为 n 的数组,缺失的最小正整数一定在 [1, n + 1] 中。 如果 1~n 中存在缺失值,答案就在其中;如果全部存在,答案就是 n + 1。所以小于等于 0 或大于 n 的数字都可以忽略。
为了不用 set,可以把原数组本身当作哈希表:
数字 1 应该放在下标 0
数字 2 应该放在下标 1
数字 3 应该放在下标 2
......
数字 x 应该放在下标 x - 1
代码如下:
python
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums) # 必定在[1, n + 1]范围内
for i in range(n):
# 大循环是下标遍历,确保每个位置的数字被"合理"放置. nums[x - 1] = x (nums[0] = 1, nums[1] = 1, ...)
while(
1 <= nums[i] <= n # 在答案区间范围内
and nums[nums[i] - 1] != nums[i] # 位置不合规
):
target_index = nums[i] - 1
nums[i], nums[target_index] = nums[target_index], nums[i] # 交换两个位置的数字
for i in range(n):
# 所有的x都被放到了index为x - 1的位置,所以遍历过去,如果nums[x - 1] != x,证明x没有出现过
if nums[i] != i + 1:
return i + 1
return n + 1 # 否则相当于一次循环排序,也即nums本身是[1, 2, 3, ..., n]的一个乱序排列
代码分析 / 解析
看了代码以后我的第一反应是:逻辑我可以理解,但是我自己撰写的时候肯定会被这个while和交换给困住,我不知道如何不重复、不遗漏的,将每个数字放在我认为的"正确"的位置上
所以思维过程应该是,首先不要想完整代码应该如何写,先定义好目标状态是什么:
先不要想完整代码,只定义目标状态:
如果数字 x 在 [1, n] 中,就尽量把它放到下标 x - 1。
例如数字 3 的目标位置是:
nums[2]
为什么需要交换
为了确保数字不会被覆盖 / 丢失。
当开始遍历,我会将遇到的数字 x(当前的处理位) 放到 index 为 x - 1的位置,如果我直接覆盖,那么原有的numsx - 1会丢失。
为了不引入额外的空间去存放numsx - 1,这里进行交换。那么此时 x 去到了正确位置,但当前处理位置变成了一个新的数字 numsx - 1
为什么是 while 不是 if
while循环确保对当前位置的处理是彻底的。当前位置被处理得当,才会进行下标的移动。
如果只使用一次 if,处理完 x 并且完成交换以后就会进入下一个下标,但当前位置的 numsx - 1 仍然没有被处理,或者说不能确定 一定被放到了正确的位置。
所以这就是while的来源,一次交换之后,当前位置会出现一个新元素;只要这个新元素还能被有效安置,就继续处理当前位置。