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 inreturn value > 0}// 排序后的都是正整数数组let sortNum = nums.sorted()var findSuccess = falsevar result = 0// 查找最小的正整数for (i,num) in sortNum.enumerated() {// 第一个数据与1比较,// 为1 就继续向后查找// 非1 就找到了最小正整数,if i == 0 {if num == 1 {continue} else {result = 1findSuccess = truebreak}} else {// 当前数与前一个对比, 可以相等(有这样的测试case[0,1,1,2,2]), 可以差为1,// 如果差大于1,那说明找到了最小的正整数let preNum = sortNum[i-1]if num - preNum > 1 {result = preNum + 1findSuccess = truebreak}}}// 如果没有在前面和中间找到最小的正整数, 那就是在最后了, 比如[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 inif value > 0 {set.insert(value)}}var result = 1while 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 = 10000var bucket = Array(repeating: 0, count: Solution.ArrayCount)func firstMissingPositive(_ nums: [Int]) -> Int {// 建一个10000个数组的桶,遍历往里面放值// 遍历这个桶,找到有空值就输出// 找不到,把输入数组每个值减9999,在重新放到桶中,// 遍历这个桶,找到有空值就输出,找不到就重复减9999,重新加桶var result = 0var count = 0var nums = numswhile true {self.fillBucket(nums)let (isSuccess, tempResult) = self.checkBucket()if isSuccess {result = tempResultbreak} else {nums = self.updateNumber(nums)self.cleabBucket()count += 1if nums.count == 0 {result = 1break}}}result = count * (Solution.ArrayCount-1) + resultreturn 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 = falsevar result = 0for (i,value) in bucket.enumerated() {if (i == 0) {continue}if (value == 0) {isSuccess = trueresult = ibreak}}return (isSuccess, result)}// 清空桶内的上一轮数据的标志位func cleabBucket() {bucket = bucket.map { _ inreturn 0}}// 原数组的数据更新func updateNumber(_ nums: [Int]) -> [Int] {var newArray = [Int]()for num in nums {if (num < Solution.ArrayCount) {// 负数, 已经往数组里放过的数,不在追加到新数组中} else {let newNum = num - Solution.ArrayCount + 1newArray.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+1let arrayCount = nums.countvar newArray: [Int] = nums.map { num invar result = numif 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 = falsevar result = 0// 第三个遍历, 找出结果for (i,num) in newArray.enumerated() {if num <= 0 {// 说明 i+1 对应的值存在} else {// 说明找到了findSuccess = trueresult = i + 1break}}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.countvar bucket = Array<Int>.init(repeating: 0, count: arrayCount)nums.forEach { num invar result = numif num >= 1 && num <= arrayCount {bucket[num-1] = 1}}var findSuccess = falsevar result = 0// 第二个遍历, 找出结果for (i,num) in bucket.enumerated() {if num == 0 {// 说明 这个数字不在桶中, 找到了findSuccess = trueresult = i + 1break}}if findSuccess == false {result = arrayCount + 1}return result}
}

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/172142.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Rust在Web开发中的应用

欢迎关注我的公众号lincyang新自媒体&#xff0c;回复关键字【程序员经典书单】&#xff0c;领取程序员的100本经典书单 大家好&#xff01;我是lincyang。 今天我们将一起深入探索Rust在Web开发领域的应用。尽管Rust最初设计用于系统编程&#xff0c;但其性能、安全性和现代并…

只狼 资源分享

版本介绍 v1.06版|容量15GB|官方简体中文|支持键盘.鼠标.手柄|赠官方原声4首BGM|赠多项修改器|赠一周目全义手忍具强化通关存档|2020年01月15号更新 只狼中文设置&#xff1a; https://jingyan.baidu.com/article/cb5d6105bc8556005d2fe048.html 只狼键盘对应按键&#xff1…

windows本地dockr的clickhouse链接本地mysql服务,连接不上

不想看过成的&#xff0c;解决办法在最后面 报错信息&#xff1a; SQL 错误 [1000] [08000]: Poco::Exception. Code: 1000, e.code() 0, Exception: Connections to all replicas failed: test1localhost:3306 as user root (version 21.12.3.32 (official build)) , serve…

C# WPF上位机开发(掌握一点c#基础)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 wpf虽然比较简单&#xff0c;但是最好还是要有一点c#的基础比较好。本身wpf有点类似于web开发&#xff0c;前端和html差不多&#xff0c;后端则和j…

SpringBoot事务处理

一、事务回顾 回顾地址&#xff1a; 深入理解数据库事务&#xff08;超详细&#xff09;_数据库事务操作_Maiko Star的博客-CSDN博客 事务&#xff1a; 是一组操作的集合&#xff0c;是一个不可分割的工作单位&#xff0c;这些操作要么同时成功&#xff0c;要么同时失败 事…

隐写-MISC-bugku-解题步骤

——CTF解题专栏—— 题目信息&#xff1a; 题目&#xff1a;隐写 作者&#xff1a;CyberFl0wer 提示&#xff1a;无 解题附件&#xff1a; 解题思路&#xff1a; 这张图片一看&#xff01;哦呦~背景还是透明的&#xff0c;那我肯定要尝试给他换换色&#xff08;不可以色色.jpg…

重生之我是一名程序员 42——字符分类函数

哈喽啊大家晚上好&#xff01;今天呢给大家带来一些超简单的知识&#xff0c;大家是需要浅浅理解就行了。所以今天给大家带来的知识是——字符分类函数。 首先呢还是给大家介绍一下它们&#xff0c;字符分类函数是一种函数&#xff0c;它根据一定的规则将字符分组或分类。在编…

Map和Set小总结【温习】

目录 一、概念与模型 二、Map的使用 三、Set的说明 一些小练习 四、哈希表 1.概念 2.冲突 2.1、概念 2.2、冲突-->避免 2.3、冲突-->解决 &#xff08;1&#xff09;闭散列 &#xff08;2&#xff09;开散列 2.4、其他问题 一、概念与模型 1.概念&#xff1a…

三菱GX WORRKS3 下载与安装

目录 下载 安装 准备好安装包 对电脑系统要求 安装 因为小编公司需要&#xff0c;所以开始了三菱plc软件的学习&#xff0c;并从今天开始记录学习&#xff0c;希望小编的内容能帮到你&#xff0c;对你的学习有帮助&#xff01; 下载 三菱电机官网 当然了&#xff0c;需要…

【C++】类和对象(下篇)

这里是目录 构造函数&#xff08;续&#xff09;构造函数体赋值初始化列表 explicit关键字隐式类型转换 static成员友元友元函数友元类 内部类匿名对象匿名对象的作用const引用匿名对象 构造函数&#xff08;续&#xff09; 构造函数体赋值 在创建对象时&#xff0c;编译器通…

五种多目标优化算法(MOGWO、MOLPB、MOJS、NSGA3、MOPSO)求解微电网多目标优化调度(MATLAB代码)

一、多目标优化算法简介 &#xff08;1&#xff09;多目标灰狼优化算法MOGWO 多目标应用&#xff1a;基于多目标灰狼优化算法MOGWO求解微电网多目标优化调度&#xff08;MATLAB代码&#xff09;-CSDN博客 &#xff08;2&#xff09;多目标学习者行为优化算法MOLPB 多目标学习…

佳易王幼儿园缴费系统软件编程应用实例

佳易王幼儿园缴费系统软件编程实例 佳易王幼儿园缴费系统功能&#xff1a; 1、系统设置 2、班级设置 3、其他费用名称 4、学生信息管理 5、学生缴费 6、统计报表 7、备份全部数据 软件试用版下载可以点击下方官网卡片

由走“贸工技”的联想联想到传统OEM,带给了自己那些思考?

2022年1月16日&#xff0c;自己来到魔都的第1597天&#xff0c;这城市还是保持着相似的容颜&#xff0c;而自己却悄悄的起了变化。 以前对时间概念其实不是特别敏感&#xff0c;感觉自己有大把的时光可以浪费&#xff08;虽然知道死亡是个永远无法逃避的话题&#xff09;&#…

MATLAB 和 Simulink 官方文档下载地址

MATLAB 官方文档中文版下载网址&#xff1a; https://ww2.mathworks.cn/help/pdf_doc/matlab/index.html 如图&#xff1a; MATLAB 官方文档英文版下载网址&#xff1a; https://ww2.mathworks.cn/help/pdf_doc/matlab/index.html?langen 如图&#xff1a; Simulink 官…

Vue - Vue配置proxy代理,开发、测试、生产环境

1、新建三个环境的配置文件 在src同级目录也就是根目录下新建文件&#xff1a;.env.development&#xff08;开发环境&#xff09;、.env.test&#xff08;测试环境&#xff09;、.env.production文件&#xff08;生产环境&#xff09; 2、三个环境的配置文件 开发环境 .env…

重量级消息,微软将ThreadX RTOS全家桶贡献给Eclipse基金会,免费供大家商用,宽松的MIT授权方式

从明年第1季度开始&#xff0c;任何人&#xff0c;任何厂家的芯片都可以免费商用&#xff0c;MIT授权就这点好。 贡献出来后&#xff0c;多方可以一起努力开发&#xff0c;当前首批兴趣小组AMD, Cypherbridge, Microsoft, NXP, PX5, Renesas, ST Microelectronics, Silicon Lab…

cephadm部署ceph quincy版本,使用ceph-csi连接

环境说明 IP主机名角色 存储设备 192.168.2.100 master100 mon,mgr,osd,mds,rgw 大于5G的空设备192.168.2.101node101mon,mgr,osd,mds,rgw大于5G的空设备192.168.2.102node102mon,mgr,osd,mds,rgw大于5G的空设备 关闭防火墙 关闭并且禁用selinux 配置主机名/etc/hosts …

HarmonyOS(五)—— 认识页面和自定义组件生命周期

前言 在前面我们通过如何创建自定义组件一文知道了如何如何自定义组件以及自定义组件的相关注意事项&#xff0c;接下来我们认识一下页面和自定义组件生命周期。 自定义组件和页面的关系 在开始之前&#xff0c;我们先明确自定义组件和页面的关系 自定义组件&#xff1a;Co…

【案例讲解】LVGL 如何用LVGL画加载圈

更多源码分析请访问:LVGL 源码分析大全 目录 1、概述2、实现效果图3、实现思路4、代码详解1、概述 很多场景下,在用户操作时,需要使用一个加载圈来缓解用户焦虑问题。 2、实现效果图 3、实现思路 用八个固定的圆点来表示加载圈,当使这八个圈依次隐藏和显示,这样就能做…

基于helm的方式在k8s集群中部署gitlab - 部署(一)

文章目录 1. 背景说明2. 你可以学到什么&#xff1f;3. 前置条件4. 安装docker服务&#xff08;所有节点&#xff09;5. 部署k8s集群5.1 系统配置&#xff08;所有节点&#xff09;5.2 安装kubelet组件(所有节点)5.2.1 编写kubelet源5.2.2 安装kubelet5.2.3 启动kubelet 5.3 集…