「前端刷题」41. 缺失的第一个正数

219 阅读1分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

题目

给你一个未排序的整数数组 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 * 105
  • -231 <= nums[i] <= 231 - 1

思路

解法一:库函数

  • 时间复杂度:O(n^2)
  • 遍历 + 内遍历
  • 空间复杂度:O(1)
  • 没有借助额外存储空间
  • 思路
  • 找数组中缺失的第一个正数
    • 那么一定是从1到n开始寻找,最多n次(即数组的长度)
    • 因此直接利用原数组的下标开始查找
  • includes 方法
    • 用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回false。
    • 实质上需要循环遍历查找,内部时间复杂度为O(n)
  • 如果1~n都出现,那就是n+1
/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    let n = nums.length;
    for(let i = 1;i <= n;i++){
        if(!nums.includes(i)){
            return i;
        }
    }
    return n + 1;
};

解法二:参考解法 - 二分查找

/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    let n = nums.length;
    nums = nums.sort((a,b) => a-b)
    let binarySearch = (val) => {
        let left = 0;
        let right = n - 1;
        while(left <= right){
            let mid = (left + right) >> 1;
            if(nums[mid] == val){
                return mid;
            }else if(nums[mid] < val){
                left = mid + 1;
            }else{
                right = mid - 1;
            }
        }
        return -1;
    }
    for(let i = 1;i <= n;i++){
        if(binarySearch(i) == -1){
            return i;
        }
    }
    return n + 1;
};

解法三:计数排序 - 经典借助额外空间版

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
  • 经典排序算法讲解 - 计数排序
  • 思路
  • 解法一:缺失的元素不能超过数组长度+1,所以设置计数数组长度最大为n,即存储的元素最大为n
  • 计数排序后
    • 遍历数组,第二个起为0的元素,说明存在第一个nuns[i] != i的元素
      • 第一个是0,自动忽略
/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    let n = nums.length;
    let bucket = new Array(n+1).fill(0);
    let sortedIndex = 0;
    for(let i = 0;i <= n;i++){
        let cell = nums[i];
        if(cell <= n){
            bucket[cell]++;
        }
    }
    for(let i = 1;i < bucket.length;i++){
        if(bucket[i] == 0){
            return i;
        }
    }
    return n+1;
};

解法四:计数排序 - 原地交换版

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 思路简析
  • 请先参考 - 经典排序算法讲解 - 计数排序 - 经典版 和 原地交换版实现
  • 其实原地交换,只是借助本身数组进行交换扩张
    • 如果num[i] != nums[nums[i]],
      • 说明需要像经典版那样以值 num[i]为索引 计数放入原数组,
      • 而因为原数组索引 nums[i] 处的值 nums[nums[i]] 可能有值,为了不漏掉,就需要交换
    • 重复上面交换动作,直至nums[i] <= 0
    • 如果需要对原数组排序,这仅仅适用于原数组没有重复元素时
      • 因为如果有重复元素,你不知道nums[nums[i]]处的值是nums[i]替换过来的,还是本身也等于这个数
      • 但此题仅仅需要找寻缺失的第一个正数,因此重复的没有关系,
        • 只记录一次就好,不需要排序,
        • 所以就不用管重复的元素,直接跳过
/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    // 省略了 nums.push(0);
    let n = nums.length;
    for(let i = 0;i <= n;i++){
        while(nums[i] > 0 && nums[i] <= n && nums[nums[i]] != nums[i]){
            [nums[nums[i]],nums[i]] = [nums[i],nums[nums[i]]];
        }
    }
    for(let i = 1;i <= n;i++){
        if(nums[i] != i){
            return i;
        }
    }
    return n + 1;
};` 

 *    或者这样写是一样的道理
  * 区别是n存在下标n里还是下标n-1里
  * 本质都是计数排序

`/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    let n = nums.length;
    for(let i = 0;i < n;i++){
        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]];
        }
    }
    for(let i = 0;i < n;i++){
        if(nums[i] != i + 1){
            return i + 1;
        }
    }
    return n + 1;
};