第一个缺失的整数

3 篇文章 0 订阅
0 篇文章 0 订阅

第一个缺失的整数

给定一个长度为N的数组A[0,…N-1],从1开始,找到第一个不在数组中的正整数。
如 [3,9,8,1,6,32]输出为2。
思路:
求解该问题可以有三种策略,分别是暴力破解法、BitMap法和循环不变式方法。
方法1—暴力破解法:
看到这个题后,既然是找从1开始第一个不在数组A中的正整数,那最简单最直接的反应就是我能否可以从1开始,在A中查找这个数是否存在,若存在则找下一个正整数,知道找到第一个不在A中的正整数。可以很明显可知,该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1),显然这不是我们想要的结果。
那么,我们进一步想,如果A是有序的话,则从头开始查找,找到第一个A[i]!=i(为简单讲解,假设数组的下标是从1开始,下同)的位置,则i即为要找的数字。因为给定的数组A并不有序,则需要首先对A进行排序,然后在对A进行遍历即可。则很明显,该算法时间复杂度主要花在对A的排序上,即使利用时间复杂度最低的排序算法,其时间复杂度仍为 O ( n ∗ l o g n ) O(n*logn) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1).
由上可知,暴力破解法的时间复杂度太高。
方法2—BitMap法:
所谓的BitMap就是用一个bit位来标记某个元素所对应的value,而key即是该元素。由于题目中并没有对算法的空间复杂度进行要求,所以我们可以进行以下操作:
首先申请一个与 A A A长度一样的空数组 B = [ 1 , . . . , N ] B=[1,...,N] B=[1,...,N] (假设数组 B B B下标是从 1 1 1开始),并将所有位置初始化为 − 1 -1 1。然后从头遍历 A A A,将 A [ i ] A[i] A[i]映射到 B B B中,具体映射方式为如果 A [ i ] &lt; 1 A[i]&lt;1 A[i]<1 或者 A [ i ] &gt; N A[i]&gt;N A[i]>N则舍弃,否则 B [ A [ i ] ] = A [ i ] B[A[i]]=A[i] B[A[i]]=A[i]。映射完成后,从 1 1 1开始遍历数组 B B B,找到第一个 B [ i ] ! = i B[i]!=i B[i]!=i的位置, i i i即为所求。
然而这是为什么呢?为什么可以这样做?
这是因为题目是从1开始找到第一个不在数组中的正整数,因为一个萝卜一个坑,只要A数组中存在一个数大于N或者小于1,那么可以很容易得出这个缺失的正整数 z z z一定满足 z &gt; = 1 并 且 z &lt; = N z&gt;= 1 并且 z&lt;=N z>=1z<=N
或者如果 A A A数组中的数都介于 1 1 1 N N N之间且互不重复,则z必等于 N + 1 N+1 N+1
所以,上述映射和查找方式是成立的。
BitMap法花费的总时间为 2 N 2N 2N,空间大小为 N N N,因此BitMap算法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度也为 O ( n ) O(n) O(n)
方法3—循环不变式法:
因为BitMap的时间复杂度已经是 O ( n ) O(n) O(n)数量级,对于此类查找算法已经不可能有数量级上的优化,即使优化也只是降低系数。但是BitMap算法的空间复杂度也是 O ( n ) O(n) O(n),那这个空间复杂度是否可以降低呢?
其实,归根到底,上述BitMap方法只是在做一件事,那就是将每一个数字都放到它应该在的位置上去。既然是这样,我们其实可以不用申请空间,而直接对原数组进行操作。即从前向后遍历数组A,将A中的每一个数放到对的位置,知道某一个位置的数据一直没有被找到,那么这个位置上的数据即为所求,上面这句话比较不好理解,那我们形式化表示一下:

假定前 i − 1 i-1 i1个数已经找到,并且依次存放在 A [ 1 , 2 , . . . . . , i − 1 ] A[1,2,.....,i-1] A[1,2,.....,i1]中,继续考察A[i],有三种情况:
(1) 若 A [ i ] A[i] A[i]<i 且 A [ i ] &gt; = 1 [i]&gt;=1 [i]>=1,则A[i]在A[1,2,…,i-1]中已经出现过,可以直接丢弃。
ps:若A[i]为负,则更应该丢弃它。
(2)若A[i]>i且A[i]<=N,则A[i]应该位于后面的位置上,则将A[A[i]]和A[i]交换。
ps:若A[i]>=N,由于缺失数据一定小于N,则丢弃A[i]。
(3)若A[i]=i,则A[i]位于正确的位置上,则i加1,循环不变式扩大,继续比较后面的元素。

对上面描述进行整理可得:
(1) 若A[i]<i 或者A[i]>N,则丢弃A[i]
(2) 若A[i]>i,则将A[A[i]]和A[i]交换。
(3) 若A[i]=i,则i加1,继续比较后面的元素。
至此,整个的算法描述已经清楚了。

那么现在有个问题,即如何对A[i]进行丢弃呢?
倒是可以直接多数组中这个位置进行删除,然后后面的所有元素往前移动,不过这样子也太费时间了。
我们可以参考堆排序或者树的剪枝操作,直接用最后一个元素A[A.size()-1]去替换A[i],然后A的长度减1。这样子就相当于对A[i]进行了丢弃,并且也不需要进行大量的其它操作,就是进行一个逻辑删除。

好了,至此整个算法的思路已经讲清楚了,下面附上循环不变式解法的python代码,供参考。

# -*- coding: utf-8 -*-
"""
Created on Sun Jan 20 15:04:13 2019

@author: shuaifeng
"""

def findFirstMissingInteger(tableA):
    if tableA is None or len(tableA)==0:
        return 1
    
    endindex=len(tableA)
    index=0
    while index<endindex:
        if tableA[index]>endindex or tableA[index]<index+1: #第一种情况,直接丢弃
            tableA[index]=tableA[endindex-1]
            endindex=endindex-1
        elif tableA[index]>index+1: #将数值放到正确的位置上去
            temp=tableA[tableA[index]-1]
            tableA[tableA[index]-1]=tableA[index]
            tableA[index]=temp
        else: #当前位置的数据在正确位置,则查找下一个
            index=index+1
    return index+1    

def main():
    A=[5,1,1,9,4,10]
    print(findFirstMissingInteger(A))

if __name__=="__main__":
    main()
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值