算法: 缺失的第一个正数

给你一个未排序的整数数组 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 * 1 0 5 10^5
  • - 2 31 2^{31}  <= nums[i] <= 2 31 2^{31}  - 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( N 2 N^2 ),空间复杂度为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)。

猜你喜欢

转载自juejin.im/post/7032967074527838216
今日推荐