leetcode 41. 缺失的第一个正数

目录:原题链接

暴力排序

桶排序

桶排序+Set

桶排序+分治思想

官方题解

桶排序+数组内标记

桶排序+额外数组标记(更好理解)


给你一个未排序的整数数组 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 * 10^5
  • -2^31 <= nums[i] <= 2^31 - 1

暴力排序

先不考虑题目要求的时间、空间复杂度,先简单暴力的做出来结果,给自己一点信心,有时候想要按照题目要求直接做出最终结果太难,先有一个能用的方案也有助于打开后续的思路。

先来暴力排序法:

  1. 对输入数据过滤后排序,
  2. 遍历一遍排序后的数组,就可以找到最小的正整数
// 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
class Solution {

    func firstMissingPositive(_ nums: [Int]) -> Int {

        // 先把不合规的 负数和0 去除
        let nums = nums.filter { value in
            return value > 0
        }
        // 排序后的都是正整数数组
        let sortNum = nums.sorted()

        var findSuccess = false
        var result = 0
        // 查找最小的正整数
        for (i,num) in sortNum.enumerated() {
            // 第一个数据与1比较,
            // 为1 就继续向后查找
            // 非1 就找到了最小正整数,
            if i == 0 {
                if num == 1 {
                    continue
                } else {
                    result = 1
                    findSuccess = true
                    break
                }
            } else {
                // 当前数与前一个对比, 可以相等(有这样的测试case[0,1,1,2,2]), 可以差为1,
                // 如果差大于1,那说明找到了最小的正整数
                let preNum = sortNum[i-1]
                if num - preNum > 1 {
                    result = preNum + 1
                    findSuccess = true
                    break
                }
            }
        }

        // 如果没有在前面和中间找到最小的正整数, 那就是在最后了, 比如[1,2,3]这样的数组
        // 有可能全是负数,过滤完之后sortNum数组为空,那1就是最小的整数
        if findSuccess == false {
            result = (sortNum.last ?? 0) + 1
        }
        return result
    }
}

暴力法用到了排序,时间复杂度为O(N*logN),空间复杂度为O(1)


桶排序

在排序算法中还有一种特殊的排序算法, 桶排序。使用桶排序的的时间复杂度为O(N),可以尝试使用桶排序的变种来做。

先不考虑空间,假设桶的空间无限

  1. 准备一个2^31大的数组作为桶bucket[]
  2. 遍历输入数据,出现数字k就bucket[k]=1标志这个数字出现;
  3. 从1开始向上找bucket中第一个出现0的数,这个数就是最小的正整数

桶排序+Set

元素的大小范围太大2^31 - 1,但是数量有限 5*10^5,可以用一个set来存所有出现的数字,然后从1开始向上枚举所有正整数,找出不在set中的数据。


// 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
class Solution {

    func firstMissingPositive(_ nums: [Int]) -> Int {

        // 元素的大小范围太大,但是数量有限,以数组的count作为集合的大小
        var set = Set<Int>.init(minimumCapacity: nums.count)
        nums.forEach { value in
            if value > 0 {
                set.insert(value)
            }
        }

        var result = 1
        while true {
            if set.contains(result) {
                result += 1
            } else {
                break
            }
        }
        return result
    }
}

这个满足时间复杂度为O(N),但是需要的空间复杂度也为O(N),同时set中的hash计算也比较费时,实际执行时间变化不大。

桶排序+分治思想

基于桶排序还有一种思路,桶排序的问题就在这个桶不能无限大,那就限定桶的大小为10000,一次分治10000条数据,针对这10000条数据在尝试用桶排序来处理,

  1. 填充桶内数据,在遍历的输入数据的时候把1-9999之间的数字k放入桶中,标记bucket[k] = 1, 超过10000(>=10000)的数据不考虑。
  2. 检查桶内数据,从1开始遍历桶中的数据,
    1. 如果在桶中找到一个值bucket[k] 为0, 那就是找到了最终结果
    2. 如果在这个桶内找不到为0的值,把原数据中的所有值都 - 9999,在重新加入桶中,重复第二步。

如果桶的范围限定为1,相当于每次查找数组的最小值,然后每次减1,退化成了选择排序法。

class Solution {

    static let ArrayCount = 10000
    var bucket = Array(repeating: 0, count: Solution.ArrayCount)

    func firstMissingPositive(_ nums: [Int]) -> Int {

        // 建一个10000个数组的桶,遍历往里面放值
        // 遍历这个桶,找到有空值就输出
        // 找不到,把输入数组每个值减9999,在重新放到桶中,
        // 遍历这个桶,找到有空值就输出,找不到就重复减9999,重新加桶
        var result = 0
        var count = 0

        var nums = nums

        while true {
            self.fillBucket(nums)
            let (isSuccess, tempResult) = self.checkBucket()
            if isSuccess {
                result = tempResult
                break
            } else {
                nums = self.updateNumber(nums)
                self.cleabBucket()
                count += 1

                if nums.count == 0 {
                    result = 1
                    break
                }
            }
        }
        result = count * (Solution.ArrayCount-1) + result
        return result
    }


    // 拿数据填充桶
    func fillBucket(_ nums: [Int]) {
        for num in nums {
            if num > 0 && num < Solution.ArrayCount {
                self.bucket[num] = 1
            }
        }
    }

    // 检查桶内有没有空值
    func checkBucket() -> (Bool, Int) {
        var isSuccess = false
        var result = 0
        for (i,value) in bucket.enumerated() {

            if (i == 0) {
                continue
            }
            if (value == 0) {
                isSuccess = true
                result = i
                break
            }
        }
        return (isSuccess, result)
    }

    // 清空桶内的上一轮数据的标志位
    func cleabBucket() {
        bucket = bucket.map { _ in
            return 0
        }
    }

    // 原数组的数据更新
    func updateNumber(_ nums: [Int]) -> [Int] {
        var newArray = [Int]()
        for num in nums {
            if (num < Solution.ArrayCount) {
                // 负数, 已经往数组里放过的数,不在追加到新数组中
            } else {
                let newNum = num - Solution.ArrayCount + 1
                newArray.append(newNum)
            }
        }
        return newArray
    }
}

虽然使用了桶排序的思想,时间复杂度为O(N*logN),空间复杂度使用了固定长度的数组,为O(1)


官方题解

最后看了官方题解,基于桶排序+使用额外set的思路,但是使用数组内的数据替换set的使用。做到了空间复杂度为O(1)。

桶排序+数组内标记

官方题解里面提到了1个重要的结论,对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1,N+1] 中。

  • 这是因为如果 [1,N]都出现了,那么答案是 N+1;比如[1,2,3]这样的数组
  • 如果出现任何一个不在[1,N]的数,都将挤占原有的一个位置,那最小正整数必定是在[1,N]中。 比如[1,5,2], [1,2,-1]

这样一来,我们将所有在 [1,N]范围内的数放入哈希表,也可以得到最终的答案。而给定的数组恰好长度为 N,这让我们有了一种将数组设计成哈希表的思路:

我们对数组进行遍历,对于遍历到的数 x,如果它在 [1,N]的范围内,那么就将数组中的第 x−1个位置(注意:数组下标从 0 开始)打上「标记」,标记x出现过。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 N+1,否则答案是最小的没有打上标记的位置加 1。

那么如何设计这个「标记」呢?由于数组中的数没有任何限制,因此这并不是一件容易的事情。但我们可以继续利用上面的提到的性质:由于我们只在意 [1,N]中的数,因此我们可以先对数组进行遍历,把不在 [1,N]范围内的数修改成任意一个大于 N 的数(例如 N+1)。这样一来,数组中的所有数就都是正数了,因此我们就可以将「标记」表示为「负号」。算法的流程如下:

  1. 我们将数组中所有小于等于 0 的数修改为 N+1;
  2. 我们遍历数组中的每一个数 x,它可能已经被打了标记,因此原本对应的数为 ∣x∣,其中 ∣∣ 为绝对值符号。如果 ∣x∣∈[1,N],那么我们给数组中的第 ∣x∣−1个位置的数添加一个负号,这个负号就是标记,标记|x| 出现过。注意如果它已经有负号,不需要重复添加;
  3. 在遍历完成之后,
    1. 如果数组中的每一个数都是负数,那么答案是 N+1,
    2. 否则答案是第一个正数的位置加 1。

class Solution {

    func firstMissingPositive(_ nums: [Int]) -> Int {

        // 第一个遍历, 把所有的负数和0标记改为 arrayCount+1
        let arrayCount = nums.count
        var newArray: [Int] = nums.map { num in
            var result = num
            if result <= 0  {
                result = arrayCount + 1
            }
            return result
        }

        // 第二个遍历, 把在[1,arrayCount]之间的数打上负数标记, 下表+1即为原始值
        for value in newArray {
            let originValye = abs(value)
            if originValye <= arrayCount {
                newArray[originValye-1] = -abs(newArray[originValye-1])
            }
        }

        var findSuccess = false
        var result = 0
        // 第三个遍历, 找出结果
        for (i,num) in newArray.enumerated() {

            if num <= 0 {
                // 说明 i+1 对应的值存在
            } else {
                // 说明找到了
                findSuccess = true
                result = i + 1
                break
            }
        }

        if findSuccess == false {
            result = arrayCount + 1
        }
        return result
    }
}

使用官方的题解确实快了不少,只需3次遍历即可完成。时间复杂度为O(N),空间复杂度为O(1)。

桶排序+额外数组标记(更好理解)

如果上面的思路没有理解到也没关系,现在的计算机内存大小一般不是瓶颈,使用额外大小的数组来做标记更好理解。

  1. 生成一个大小为N的桶,初始化内部元素为0
  2. 遍历输入数据,在[1,N]之间的数字加入桶中,标记为1
  3. 遍历桶中数据,
    1. 出现第一个标记为0的元素下标+1即为结果。
    2. 没有出现为0的元素,N+1即为结果。
class Solution {

    func firstMissingPositive(_ nums: [Int]) -> Int {
        // 第一个遍历, 把[1,N]之间的数字放入桶中, 并做好标记
        let arrayCount = nums.count
        var bucket = Array<Int>.init(repeating: 0, count: arrayCount)
        nums.forEach { num in
            var result = num
            if num >= 1 && num <= arrayCount {
                bucket[num-1] = 1
            }
        }

        var findSuccess = false
        var result = 0
        // 第二个遍历, 找出结果
        for (i,num) in bucket.enumerated() {

            if num == 0 {
                // 说明 这个数字不在桶中, 找到了
                findSuccess = true
                result = i + 1
                break
            }
        }

        if findSuccess == false {
            result = arrayCount + 1
        }
        return result
    }
}

这个满足时间复杂度为O(N),但是需要的空间复杂度也为O(N),相当于对set方案的一次深度优化,

优化点1:使用数组的偏移替换复杂的hash计算。

优化点2:充分利用下面结论,减少了不必要的数据处理。

对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1,N+1] 中。

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值