给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
示例 1:
输入: nums = [1,2,0]
输出: 3
复制代码
示例 2:
输入: nums = [3,4,-1,1]
输出: 2
复制代码
示例 3:
输入: nums = [7,8,9,11,12]
输出: 1
复制代码
提示:
- 1 <= nums.length <= 5 *
- - <= nums[i] <= - 1
要正确找出第一个缺失的正数,第一时间没想到哈希表,本题就比较难解决。首先想到的方案是记录有效区域,在遍历nums的同时动态提交有效区域, 例如初始化有效区域ranges为[[1, Infinity]], 以nums[3, 4, -1, 1]为例:
- 第一个数为3, ranges拆分为[[1, 3], [4, Inifinity]]
- 第二个数为4,ranges调整为[[1, 3], [5, Inifinity]]
- ...
遍历完所有的数据,ranges最终为[[2, 3], [5, Inifinity]],那么ranges数组第一项的最小值即为我们要求的”缺失的第一个正数“。
/**
* @param {number[]} nums
* @return {number}
*/
var firstMissingPositive = function(nums) {
// 定义有效范围区间,初始区间为所有正整数
let ranges = [[1, Infinity]]
for (let vi = 0; vi < nums.length; vi++) {
const value = nums[vi]
// 遍历有效范围,和value进行匹配
for (let rgej = 0; rgej < ranges.length; rgej++) {
const min = ranges[rgej][0], max = ranges[rgej][1]
// 如果value位于当前区间内,需要将当前区间拆分为[min, value - 1]和[value + 1, max]
if (min < value && value < max) {
ranges.splice(rgej, 1, [min, value - 1], [value + 1, max])
// 一分为二,那么索引要跳过,减少无效判断
rgej++
} else {
if (min === value) {
ranges[rgej] = [min + 1, max]
} else if (max === value) {
ranges[rgej] = [min, max - 1]
}
// 如果调整后有无效的区间,例如[4,3],则从ranges移除并调整索引
if (ranges[rgej][0] > ranges[rgej][1]) {
ranges.splice(rgej, 1)
rgej--
}
}
}
}
return ranges[0][0]
};
复制代码
此方法需要动态调整ranges, 并且判断次数也比较多,例如当前value如果在(min, max)区间内,需要将(min, max)区间拆分为[min, value -1]、[value + 1, max]。
比较理想的情况,ranges长度一直为1,例如nums为[1, 2, 3, 4], 最终ranges变为[[5, Infinity]]。时间复杂度为O(N), 空间复杂度为O(1)。
最坏的情况,ranges长度最终变为nums.length + 1,例如nums为[3, 10, 5, 8], 最终ranges变为[[1, 2], [4, 4], [6, 7], [9, 9], [11, Infinity]]。时间复杂度为O(
),空间复杂度为O(N)。
以上方法不满足题目要求,前面有提到如果本题有想到哈希表,那么解法就比较简单了,思考缺失的第一个正数存在的情况,假如数组长度为n,从1到n共包含n个正数1,2,3,...n,只要数组存在重复或者不在1到n范围内,那么缺失的第一个正数肯定在[1,n]范围内,如果nums的值正好为[1,2,...,n],那么我们知道要求的数即为n + 1。
假如定义长度为n的哈希表,其索引为1到n连续数字,如果nums中位置i的数字在1到n范围内,可把哈希表索引nums[i - 1]处的值用特殊数字标示,那么最终第一个没有标示的索引即为我们要求的数字。
例如nums为[3, 4, -1, 1],标示字后的哈希表为[-1, 0, -1, -1],-1为特殊标示,那么第一个没有被表示的索引为1,要求的数字即为2(索引从0开始)。
但题目要求空间复杂度为O(1),所以我们不能单独创建哈希表,可结合哈希表思路直接在原数组上修改标示。假如我们统一用负数表示当前位置已经被占用,需要先将原来的负数转换为不在[1, n]范围的任意正数,例如n + 1。还是以[3, 4, -1, 1]为例:
- 先将-1调整为5,数组变为[3, 4, 5, 1],遍历数组。
- 第一个值为3, 将索引2(索引从0开始)位置变为负数,数组变为[3, 4, -5, 1]。
- 第二个值为4,将索引3位置变为负数,数组变为[3, 4, -5, -1]。
- 第三个值为-5,求绝对值找到原数值5,不在[1, n]范围,继续往下遍历。
- 第四个值为-1,原始值为1, 数组变为[-3, 4, -5, -1]。
- 再次遍历数组找到第一个为正数的索引i,那么i + 1即为要求的数字。如果不存在正数,那么要求的数值为n + 1。
/**
* @param {number[]} nums
* @return {number}
*/
var firstMissingPositive = function(nums) {
// 第一个缺失的正整数肯定在[1, n+1]内,当nums中数据都包含在[1, n],那第一个缺失的正整数就为n + 1
// 可以考虑用哈希表表示
const len = nums.length
// 将所有小于等于0的数统一赋值为 len + 1,后续统一用负数来表示hash表对应索引被占用
for (let i = 0; i < len; i++) {
if (nums[i] <= 0) {
nums[i] = len + 1
}
}
// 如果值在[1, len]范围内,在对应的[val - 1]索引标示为-abs(value),标示 val值已经存在.
for (let i = 0; i < len; i++) {
if (0 < Math.abs(nums[i]) && Math.abs(nums[i]) <= len) {
const tagIndex = Math.abs(nums[i]) - 1
nums[tagIndex] = -Math.abs(nums[tagIndex])
}
}
// 遍历找到第一个不为-1的值,该值对应的索引即为我们要求得的缺失的第一个整数
for (let i = 0; i < len; i++) {
if (nums[i] > 0) {
return i + 1
}
}
return len + 1
};
复制代码
该方法的时间复杂度为O(N),空间复杂度为O(1)。
类似的思路,我们还可以考虑置换法,将数字放到[1, n]对应的正确位置,例如将数字x(在[1, n]范围内)存放到对应的索引位置:nums[x - 1] = x。那么最终第一个和数字对应不上的索引即为求得的数字。例如nums数组[3, 4, -1, 1],遍历情况:
- 索引0,值为3, 将3存放到正确的位置并和对应位置的值置换,nums[2] = 3, nums[0] = -1。
- 索引0,值为-1,不在范围[1 n],往下遍历。
- 索引1,值为4,将4存放到正确的位置并置换,nums[3] = 4, nums[1] = 1。
- 索引1,值为1,将1存放到正确的位置并置换,nums[0] = 1, nums[1] = -1,-1不在范围[1,n],往下遍历。
- ...
最终得到的数组为[1, -1, 3, 4],第2个位置和索引不一致,所以2即为要求的数字。
/**
* @param {number[]} nums
* @return {number}
*/
var firstMissingPositive = function(nums) {
// 置换法,将val在[1, len]范围内的数值置换到正确的位置,当所有数据都置换完成后,重新遍历数组
// 第一个val和索引不对应的位置即为需要找的第一个缺失正整数
const len = nums.length
for (let i = 0; i < len; i++) {
// [3,4,-1,1], 将3存放到nums[3 - 1],将4存放到[4 - 1],-1跳过, 1存放到[1 - 1]
// 需要考虑占用的问题,例如4存放到nums[4 - 1],但原来索引3位置存放的数1也要考虑存放到正确的位置
// 当nums[i]不在[1, len]或者nums[i] = i + 1时,将终止当前替换
while (1 <= nums[i] && nums[i] <= len && nums[nums[i] - 1] !== nums[i]) {
const tempVal = nums[nums[i] - 1]
nums[nums[i] - 1] = nums[i]
nums[i] = tempVal
}
}
for (let i = 0; i < len; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
return len + 1
};
复制代码
在置换的时候需要一直循环,直到当前存放的数字不在[1, n]范围,如果数组每个索引位置都能和值对应上,那么求得数值即为n + 1。虽然函数中有用到while循环,循环过程会提前把值存放到正确的位置,所以后续的遍历while会直接跳过。最终的时间复杂度还是O(N),空间复杂度为O(1)。